🎓 Maestría en Inteligencia Artificial Aplicada¶

Institution Course Activity


🖼️ Visión Computacional para Imágenes y Video¶

👨‍🏫 Profesores¶

  • Profesor Titular: Dr. Gilberto Ochoa Ruiz
  • Profesor Asistente: MIP Ma. del Refugio Melendez Alfaro
  • Profesor Tutor: M. en C. Jose Angel Martinez Navarro

Laboratorio 7.2: Harris Corner Detection¶

Extracción de Características - Detección de Puntos de Interés¶

📌 Detalles de la Actividad¶

  • Código: 7.2 Google Colab
  • Título: Harris Corner Detection - Feature Detection
  • Fecha de entrega: 📅 Octubre 26, 2025 a las 23:59
  • Formato de entrega: Notebook (.ipynb) + Informe
  • Modalidad: Equipo

👥 Team 13¶

🚀 Nuestro Equipo¶

Javier Augusto Rebull Saucedo¶

Javier Augusto Rebull Saucedo

Matrícula: A01795838
🎓 MNA Student

Juan Carlos Pérez Nava¶

Juan Carlos Pérez Nava

Matrícula: A01795941
🎓 MNA Student

Luis Gerardo Sánchez Salazar¶

Luis Gerardo Sánchez Salazar

Matrícula: A01232963
🎓 MNA Student

Oscar Enrique García García¶

Oscar Enrique García García

Matrícula: A01016093
🎓 MNA Student


Objetivo del Proyecto¶

El detector de Harris es un algoritmo fundamental en visión computacional para la detección de características locales (feature detection), siendo el primer paso esencial en el pipeline de:

  • Extracción de características (Feature Detection & Description)
  • Correspondencia entre imágenes (Image Matching)
  • Reconstrucción 3D (Structure from Motion)
  • Seguimiento de objetos (Object Tracking)
  • Calibración de cámaras (Camera Calibration)
  • Reconocimiento visual (Visual Recognition)

El método de Harris (1988) detecta puntos de interés (keypoints) que son invariantes a rotación y traslación, siendo la base para algoritmos modernos como SIFT, SURF y ORB.

En este laboratorio implementaremos el detector de Harris desde cero usando Python y NumPy, analizando:

  • El comportamiento del algoritmo bajo diferentes condiciones (iluminación, ángulo, escala)
  • El sistema de umbrales adaptativos para detección robusta
  • Las fortalezas y limitaciones del método Harris
  • Comparación visual entre diferentes configuraciones de parámetros

📚 Tabla de Contenidos¶

  1. Configuración e Instalación
  2. Descarga y Preparación del Dataset
  3. Implementación del Algoritmo Harris
    • 3.1 Conversión a Escala de Grises
    • 3.2 Cálculo de Derivadas Espaciales (Gradientes)
    • 3.3 Configuración del Tensor de Estructura
    • 3.4 Cálculo de la Respuesta Harris
    • 3.5 Detección de Esquinas y Bordes
  4. Análisis por Objeto
    • 4.1 Totoro 🐱
    • 4.2 Borrego Tec 🐏
    • 4.3 Trumpy 🎺
    • 4.4 Amlito 🌮
    • 4.5 Jack Daniels 🥃
    • 4.6 Simbita 🦁
    • 4.7 Cabina London ☎️
    • 4.8 All Friends 👥
  5. Tablas Comparativas y Visualizaciones
    • 5.1 Tabla Consolidada de Resultados
    • 5.2 Gráficos de Caja (Box Plots)
    • 5.3 Análisis Estadístico
  6. Conclusiones y Análisis de Resultados
    • 6.1 Efecto de Iluminación
    • 6.2 Efecto de Ángulo de Captura
    • 6.3 Efecto de Escala (Zoom)
    • 6.4 Fortalezas y Limitaciones
    • 6.5 Comparación con Otros Métodos
  7. Reflexión Final de Equipo
  8. Referencias (APA 7)

🔑 Conceptos Clave¶

Concepto Descripción
Corner (Esquina) Punto donde la intensidad cambia significativamente en múltiples direcciones
Edge (Borde) Punto donde la intensidad cambia en una sola dirección
Respuesta Harris (R) $R = \det(M) - k \cdot \text{trace}(M)^2$
Tensor de Estructura (M) Matriz que captura gradientes locales en ventana gaussiana
Sistema Híbrido Combina criterio estadístico + percentil para umbrales adaptativos
Invarianza Harris es invariante a rotación/traslación, NO a escala

📊 Dataset¶

  • Total de imágenes: 48 imágenes + 1 demo (chessboard)
  • Objetos: 8 diferentes (Totoro, Borrego Tec, Trumpy, Amlito, Jack Daniels, Simbita, Cabina London, All Friends)
  • Condiciones por objeto: 6 variaciones
    • 📸 Vista frontal normal
    • 🌙 Vista frontal oscura
    • ➡️ Lateral derecho
    • ⬅️ Lateral izquierdo
    • 🔍 Zoom (acercamiento)
    • 🔄 Otras perspectivas
  • Resolución: 800×600 píxeles (480,000 píxeles totales)

🛠️ Tecnologías Utilizadas¶

Python NumPy OpenCV SciPy Matplotlib Pandas Google Colab


Última actualización: Octubre 2025
Versión del notebook: 1.0

🎯 Laboratorio 7.2: Harris Corner Detection¶

Extracción de Características - Detección de Puntos de Interés¶


📋 Contexto del Problema¶

En visión computacional, uno de los desafíos fundamentales es encontrar correspondencias entre imágenes que muestran el mismo objeto o escena bajo diferentes condiciones. Este problema es crucial porque las imágenes pueden sufrir múltiples transformaciones:

  • Geométricas: Cambios de perspectiva, rotación, escala, traslación
  • Fotométricas: Variaciones de iluminación, contraste, exposición

🎯 Pipeline de Extracción de Características¶

El proceso general para encontrar correspondencias se divide en tres etapas:

  1. Detección (Detection): Identificar puntos de interés (keypoints) distintivos
  2. Descripción (Description): Calcular un descriptor local para cada keypoint
  3. Correspondencia (Matching): Comparar descriptores entre imágenes

En este laboratorio nos enfocaremos en la primera etapa: Detección, utilizando el algoritmo de Harris Corner Detector.


🔍 ¿Qué es el Detector de Harris?¶

El Harris Corner Detector (desarrollado por Chris Harris y Mike Stephens en 1988) es un operador matemático que detecta "esquinas" (corners) en una imagen. Una esquina se define como una región donde la intensidad de la imagen cambia significativamente en todas las direcciones.

Distinción de Regiones:¶

  • Región plana: No hay cambio de intensidad en ninguna dirección
  • Borde (Edge): Cambio de intensidad en una sola dirección
  • Esquina (Corner): Cambio de intensidad en múltiples direcciones ✅

Propiedades del Detector de Harris:¶

  • ✅ Covariante a traslación y rotación
  • ❌ NO invariante a cambios de escala (limitación principal)
  • ✅ Robusto a cambios moderados de iluminación

🎓 Objetivos del Laboratorio¶

  1. Implementar el algoritmo de Harris Corner Detection desde cero
  2. Analizar el comportamiento del detector bajo diferentes condiciones:
    • Diferentes ángulos de captura
    • Variaciones de iluminación (claro vs oscuro)
    • Cambios de escala (zoom)
    • Diferentes perspectivas
  3. Comparar resultados con diferentes configuraciones de parámetros
  4. Evaluar las fortalezas y debilidades del método Harris

📚 Referencias¶

  • Harris, C., & Stephens, M. (1988). "A Combined Corner and Edge Detector"
  • OpenCV Feature Detection Tutorial: https://docs.opencv.org/3.4/db/d27/tutorial_py_table_of_contents_feature2d.html
  • Dr. Gilberto Ochoa Ruiz - Módulo 3.2: Extracción de Descriptores

📑 Tabla de Contenidos¶

  1. Configuración e Instalación
  2. Descarga de Dataset
  3. Implementación del Algoritmo Harris
    • 3.1 Conversión a Escala de Grises
    • 3.2 Cálculo de Derivadas Espaciales (Gradientes)
    • 3.3 Configuración del Tensor de Estructura
    • 3.4 Cálculo de la Respuesta Harris
    • 3.5 Detección de Esquinas y Bordes
  4. Análisis por Objeto
  5. Tablas Comparativas
  6. Conclusiones

1. 🔧 Configuración e Instalación ¶

Importamos las bibliotecas necesarias para el procesamiento de imágenes y el análisis numérico.

In [1]:
# Instalación de dependencias
!pip install gdown -q

# Librerías estándar
import os
import gdown
import pandas as pd
import numpy as np
from pathlib import Path
import shutil
from collections import defaultdict

# Procesamiento de imágenes
import cv2
from scipy import signal as sig
from scipy.ndimage.filters import convolve
from PIL import Image

# Visualización
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.gridspec import GridSpec
import seaborn as sns

# Configuración de visualización
plt.rcParams['figure.figsize'] = (15, 8)
plt.rcParams['font.size'] = 10
sns.set_style("whitegrid")

print("✅ Librerías importadas correctamente")
print(f"📦 OpenCV version: {cv2.__version__}")
print(f"📦 NumPy version: {np.__version__}")
/tmp/ipython-input-2298597382.py:16: DeprecationWarning: Please import `convolve` from the `scipy.ndimage` namespace; the `scipy.ndimage.filters` namespace is deprecated and will be removed in SciPy 2.0.0.
  from scipy.ndimage.filters import convolve
✅ Librerías importadas correctamente
📦 OpenCV version: 4.12.0
📦 NumPy version: 2.0.2

2. 📥 Descarga y Preparación del Dataset ¶

Descargamos las imágenes desde Google Drive utilizando gdown. El dataset incluye:

  • Imagen Demo: Tablero de ajedrez (chessboard) - ideal para detectar esquinas
  • 8 Objetos diferentes capturados bajo múltiples condiciones:
    • Iluminación: Normal y oscura
    • Ángulos: Frontal, lateral izquierdo, lateral derecho
    • Escala: Vista normal y zoom
    • Otras perspectivas

🎯 Objetos del Dataset:¶

  1. Totoro 🐱
  2. Borrego Tec 🐏
  3. Trumpy 🎺
  4. Amlito 🌮
  5. Jack Daniels 🥃
  6. Simbita 🦁
  7. Cabina London ☎️
  8. All Friends 👥
In [2]:
# Crear estructura de directorios
data_dir = Path('data')
data_dir.mkdir(exist_ok=True)

# Directorios por objeto
objects = ['01_Totoro', '02_BorregoTec', '03_Trumpy', '04_Amlito',
           '05_JackDaniels', '06_Simbita', '07_CabinaLondon', '08_AllFriends']

for obj in objects:
    (data_dir / obj).mkdir(exist_ok=True)

print("✅ Estructura de directorios creada")
✅ Estructura de directorios creada
In [3]:
# Datos del CSV (imágenes y sus IDs de Google Drive)
image_data = {
    'nombre_archivo': [
        '7.2_Characteristics_Extraction_chessboard.jpg',
        '01_Totoro_01_Totoro_Front.jpg',
        '01_Totoro_02_Totoro_Front_Dark.jpg',
        '01_Totoro_03_Totoro_Lateral_Derecho.jpg',
        '01_Totoro_04_Totoro_Lateral_Izquierdo.jpg',
        '01_Totoro_05_Totoro_Zoom.jpg',
        '01_Totoro_06_Totoro_Other.jpg',
        '02_BorregoTec_01_BorregoTec_Front.jpg',
        '02_BorregoTec_02_BorregoTec_dark.jpg',
        '02_BorregoTec_03_BorregoTec_Lateral_Derecho.jpg',
        '02_BorregoTec_04_BorregoTec_Lateral_Izquierdo.jpg',
        '02_BorregoTec_05_BorregoTec_Zoom.jpg',
        '02_BorregoTec_06_BorregoTec_Lateral_Other.jpg',
        '03_Trumpy_01_Trumpy_Front.jpg',
        '03_Trumpy_02_Trumpy_Front_Dark.jpg',
        '03_Trumpy_03_Trumpy_Latera_Derecho.jpg',
        '03_Trumpy_03_Trumpy_Latera_Izquierdo.jpg',
        '03_Trumpy_05_Trumpy_Zoom.jpg',
        '03_Trumpy_06_Trumpy_Latera_Other.jpg',
        '04_Amlito_01_Amlito_Front.jpg',
        '04_Amlito_02_Amlito_Front_Dark.jpg',
        '04_Amlito_03_Amlito_Lateral_Derecho.jpg',
        '04_Amlito_04_Amlito_Lateral_Izquierdo.jpg',
        '04_Amlito_05_Amlito_Zoom.jpg',
        '04_Amlito_06_Amlito_Other.jpg',
        '05_JackDaniels_01_JackDaniel_Front.jpg',
        '05_JackDaniels_02_JackDaniel_Front_Dark.jpg',
        '05_JackDaniels_03_JackDaniel_Lateral_Derecho.jpg',
        '05_JackDaniels_04_JackDaniel_Lateral_Izquierdo.jpg',
        '05_JackDaniels_05_JackDaniel_Zoom.jpg',
        '05_JackDaniels_06_JackDaniel_Zoom.jpg',
        '06_Simbita_01_Simbita_Front.jpg',
        '06_Simbita_02_Simbita_Front_Dark.jpg',
        '06_Simbita_03_Simbita_Lateral_Derecho.jpg',
        '06_Simbita_04_Simbita_Lateral_Izquierdo.jpg',
        '06_Simbita_05_Simbita_Lateral_Zoom.jpg',
        '06_Simbita_06_Simbita_Other.jpg',
        '07_CabinaLondon_01_LondonPhone_Front.jpg',
        '07_CabinaLondon_02_LondonPhone_Front_Dark.jpg',
        '07_CabinaLondon_03_LondonPhone_Lateral_Derecho.jpg',
        '07_CabinaLondon_04_LondonPhone_Lateral_Izquierdo.jpg',
        '07_CabinaLondon_05_LondonPhone_Lateral_Zoom.jpg',
        '07_CabinaLondon_06_LondonPhone_Other.jpg',
        '08_AllFriends_01_AllFriends.jpg',
        '08_AllFriends_02_AllFriends.jpg',
        '08_AllFriends_03_AllFriends.jpg',
        '08_AllFriends_04_AllFriends.jpg',
        '08_AllFriends_05_AllFriends.jpg',
        '08_AllFriends_06_AllFriends.jpg'
    ],
    'id_archivo': [
        '1BF_FznGku-lPAvXmSEJYoAfyTZeKSOl4',
        '17ZsTctI7UnnAX6SjSEEiir8AYr7wwRUu',
        '11SrPWXTzGV9FFy3O9rHiX8iC6KxgWUZR',
        '1ocgQo98lWu6PlqLODq-NzCRQXROi9h-d',
        '1gLHotGjFi3DLZ-udZS-6rOQVBamtcjKK',
        '1ZrijkckHyRti0T6ZWi9Sq-wt_si-g-82',
        '1dC_kTWr3pUjmvmINatctkH8PQjnAIEby',
        '15fTnfAVEbR9wEexW3p_g_NiiGtjhmGGq',
        '19AwTTtt_-8dqJjvFlf0xar9jSEWByJA7',
        '1RxenQTKirK4_DWZT5toqZ0sClq-yRCza',
        '1Oodlm15TdlxgKpVX3iQ5VgBFRUeS5u3j',
        '1dsooXHr1I6j1Dodw2O5DLcqU_A34HbUJ',
        '1GJjlMxXb7ARJhWWcoXDghHq7SvJ8vFnF',
        '1Z0nkRn_R7lC8hsFcwAVQlOEzs2FRvL9-',
        '1AEaaWwXnxT-3KmBQP5EQdT5dbzLJdtLh',
        '1tWxzx445HkAHDuhoCUVXugwUH6MbQfQr',
        '193DkPWUwHcjNBUxirwUDHUwZVkAlJQUe',
        '1fxNjAlXG6GYkIA-ACNgfR-WXKEnqkooQ',
        '1wUUyi7993Ty9DWlm1THQhtWG0-KjluvN',
        '1jG9piTePQ3u3VJ3OcSTdW-OrHmnAwLFx',
        '17s9p-tzfUslQ1JihKwcB9NDVYWNUINTe',
        '12MinhNNYdJUamHldK1wNLE1csA6wQRrs',
        '1wFZXKV5c0RN5yHZ5pZZCiWiwCvFEq5p9',
        '1rdTQM3iOGfeQFWez-Jd6GdEDiBcTBj5S',
        '1mWJchSLnsm7eYGP7ae-fyGu-VJSxdPP9',
        '1SZ7ubx8TeELNX0gWJuwa7CNp4mzr79gC',
        '1tpxgqJu2_C0SBgv7K9_Xhx26dx0tnu2F',
        '1JYNtgVFYsUI-8zUTy0cS148JSM4aQiss',
        '1kJJormn6waOeyR-1ah40tjaJ5mzCY4Iq',
        '1-TaQmC2Lrf0LcsOoYkIHxdrWLD6Fq1VD',
        '1EiJBK7ELnFFhE4GiJkDZU7VGEqWkIb9E',
        '15k0E1w0pcpfjY5Ya-iMlrcEr0olfgf-r',
        '13gspVjsuf2dSaYJwNlGrwDsqXBFPjGUH',
        '1lptaj05KxHiEnn1enEVoV12_1vIZeQ0k',
        '1KKGaF7563g2WMeJGg272CsW56-5zPFIt',
        '1-n4SqFhnYg7N0iHbJfBwKTnZjZ3mg2rj',
        '1EQpeyyHCWt7L0ZPq7vve9XQ3lsggfQOg',
        '1Uq-w02e-fiYrd09JRJ5xVm7ACKELw8ir',
        '1BArjDQWFIYkb_qOQh7ErCkvYejnJEOa2',
        '1l2jN8xkvF6STAl0m1yxgRA7Vhd5SkW1i',
        '1bqNshlrvEv9KXW4nXEFaFj367FQ9ZDW8',
        '1yqNGwlBWgYaLMHB5y0QIAaIoerBidorD',
        '1Ho4FeVT9srj0UM1mkm7j6-C2pXfjfw_N',
        '1Jig8jKnv0WpR_jYCsHozmu9VhgBRCE0h',
        '1mEARcofi2vVgTHsYlCz1nCaBWq8iHB3d',
        '1yPJkA_c4RF8BuJEglI7vqs_xxLaUsp3l',
        '19igYU1t3zhI79PXTZC7VM7FdhkUNhpvV',
        '1NfxzEMd6I1GjbhXeE0ANp9HB5DULQDcF',
        '13F54duRAXmYEUsBaeZf-4LaH3GslFShA'
    ]
}

df_images = pd.DataFrame(image_data)
print(f"✅ Dataset: {len(df_images)} imágenes registradas")
df_images.head(10)
✅ Dataset: 49 imágenes registradas
Out[3]:
nombre_archivo id_archivo
0 7.2_Characteristics_Extraction_chessboard.jpg 1BF_FznGku-lPAvXmSEJYoAfyTZeKSOl4
1 01_Totoro_01_Totoro_Front.jpg 17ZsTctI7UnnAX6SjSEEiir8AYr7wwRUu
2 01_Totoro_02_Totoro_Front_Dark.jpg 11SrPWXTzGV9FFy3O9rHiX8iC6KxgWUZR
3 01_Totoro_03_Totoro_Lateral_Derecho.jpg 1ocgQo98lWu6PlqLODq-NzCRQXROi9h-d
4 01_Totoro_04_Totoro_Lateral_Izquierdo.jpg 1gLHotGjFi3DLZ-udZS-6rOQVBamtcjKK
5 01_Totoro_05_Totoro_Zoom.jpg 1ZrijkckHyRti0T6ZWi9Sq-wt_si-g-82
6 01_Totoro_06_Totoro_Other.jpg 1dC_kTWr3pUjmvmINatctkH8PQjnAIEby
7 02_BorregoTec_01_BorregoTec_Front.jpg 15fTnfAVEbR9wEexW3p_g_NiiGtjhmGGq
8 02_BorregoTec_02_BorregoTec_dark.jpg 19AwTTtt_-8dqJjvFlf0xar9jSEWByJA7
9 02_BorregoTec_03_BorregoTec_Lateral_Derecho.jpg 1RxenQTKirK4_DWZT5toqZ0sClq-yRCza
In [4]:
def download_and_resize_image(file_id, output_path, max_size=800):
    """
    Descarga una imagen desde Google Drive y la redimensiona para optimizar espacio.

    Args:
        file_id: ID del archivo en Google Drive
        output_path: Ruta donde guardar la imagen
        max_size: Tamaño máximo en píxeles (se mantiene aspect ratio)
    """
    try:
        # Descargar imagen temporal
        temp_path = 'temp_image.jpg'
        url = f'https://drive.google.com/uc?id={file_id}'
        gdown.download(url, temp_path, quiet=True)

        # Cargar y redimensionar
        img = Image.open(temp_path)

        # Calcular nuevo tamaño manteniendo aspect ratio
        width, height = img.size
        if max(width, height) > max_size:
            if width > height:
                new_width = max_size
                new_height = int(height * (max_size / width))
            else:
                new_height = max_size
                new_width = int(width * (max_size / height))

            img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)

        # Guardar optimizada
        img.save(output_path, 'JPEG', quality=85, optimize=True)

        # Eliminar temporal
        os.remove(temp_path)

        return True
    except Exception as e:
        print(f"❌ Error descargando {output_path}: {e}")
        return False

# Descargar todas las imágenes
print("📥 Descargando imágenes...\n")
downloaded = 0

for idx, row in df_images.iterrows():
    filename = row['nombre_archivo']
    file_id = row['id_archivo']

    # Determinar carpeta de destino
    if 'chessboard' in filename:
        output_path = data_dir / filename
    else:
        # Extraer prefijo del objeto
        prefix = filename.split('_')[0] + '_' + filename.split('_')[1]
        output_path = data_dir / prefix / filename

    if download_and_resize_image(file_id, str(output_path)):
        downloaded += 1
        if (downloaded % 5 == 0):
            print(f"✓ Descargadas: {downloaded}/{len(df_images)}")

print(f"\n✅ Descarga completada: {downloaded}/{len(df_images)} imágenes")
print(f"📁 Imágenes guardadas en: {data_dir.absolute()}")
📥 Descargando imágenes...

✓ Descargadas: 5/49
✓ Descargadas: 10/49
✓ Descargadas: 15/49
✓ Descargadas: 20/49
✓ Descargadas: 25/49
✓ Descargadas: 30/49
✓ Descargadas: 35/49
✓ Descargadas: 40/49
✓ Descargadas: 45/49

✅ Descarga completada: 49/49 imágenes
📁 Imágenes guardadas en: /content/data

3. 🔬 Implementación del Algoritmo Harris ¶

El algoritmo de Harris se basa en el análisis de la matriz de estructura (también llamada matriz de segundo momento) de una imagen. Esta matriz captura la variación de intensidad en diferentes direcciones alrededor de cada píxel.

📐 Fundamento Matemático¶

Para cada píxel, se construye una matriz de estructura $M$:

$$M = \begin{bmatrix} I_x^2 & I_x I_y \\ I_x I_y & I_y^2 \end{bmatrix}$$

Donde:

  • $I_x$ = Derivada de la imagen en dirección x (gradiente horizontal)
  • $I_y$ = Derivada de la imagen en dirección y (gradiente vertical)

La función de respuesta Harris se calcula como:

$$R = det(M) - k \cdot trace(M)^2$$

Donde:

  • $det(M) = I_x^2 \cdot I_y^2 - (I_x I_y)^2$ (determinante)
  • $trace(M) = I_x^2 + I_y^2$ (traza)
  • $k$ = constante empírica (típicamente 0.04 - 0.06)

Interpretación de R:¶

  • R > 0 → Esquina (corner) ✅
  • R < 0 → Borde (edge)
  • R ≈ 0 → Región plana

Ahora implementaremos cada paso del algoritmo de forma modular.

3.1 📸 Conversión a Escala de Grises ¶

El primer paso es convertir la imagen a escala de grises. Esto simplifica el procesamiento al reducir la imagen a un solo canal de intensidad.

¿Por qué escala de grises?

  • Reduce la complejidad computacional (de 3 canales a 1)
  • Los gradientes de intensidad son suficientes para detectar esquinas
  • El algoritmo de Harris trabaja con derivadas de intensidad

Utilizaremos la imagen de chessboard (tablero de ajedrez) como demostración, ya que contiene esquinas bien definidas ideales para el detector de Harris.

In [5]:
# Cargar imagen demo (chessboard)
chessboard_path = str(data_dir / '7.2_Characteristics_Extraction_chessboard.jpg')
img = cv2.imread(chessboard_path)
img_color = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Visualización
fig, axes = plt.subplots(1, 2, figsize=(15, 6))

axes[0].imshow(img_color)
axes[0].set_title('Imagen Original (RGB)', fontsize=14, fontweight='bold')
axes[0].axis('off')

axes[1].imshow(img_gray, cmap='gray')
axes[1].set_title('Imagen en Escala de Grises', fontsize=14, fontweight='bold')
axes[1].axis('off')

plt.tight_layout()
plt.show()

print(f"📏 Dimensiones de la imagen: {img_gray.shape}")
print(f"📊 Rango de valores: [{img_gray.min()}, {img_gray.max()}]")
No description has been provided for this image
📏 Dimensiones de la imagen: (612, 612)
📊 Rango de valores: [0, 255]

3.2 📊 Cálculo de Derivadas Espaciales (Gradientes) ¶

Las derivadas espaciales (gradientes) nos indican dónde y en qué dirección cambia la intensidad de la imagen.

Operador Sobel¶

Utilizamos el operador Sobel, que combina suavizado Gaussiano con derivación:

Kernel para gradiente en X (horizontal): $$S_x = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{bmatrix}$$

Kernel para gradiente en Y (vertical): $$S_y = \begin{bmatrix} 1 & 2 & 1 \\ 0 & 0 & 0 \\ -1 & -2 & -1 \end{bmatrix}$$

Estos kernels detectan cambios de intensidad:

  • $I_x$: Bordes verticales (cambios horizontales)
  • $I_y$: Bordes horizontales (cambios verticales)
In [6]:
def gradient_x(img_gray):
    """
    Calcula el gradiente en dirección X usando el operador Sobel.
    Detecta bordes verticales (cambios de intensidad horizontal).
    """
    kernel_x = np.array([[-1, 0, 1],
                         [-2, 0, 2],
                         [-1, 0, 1]])
    return sig.convolve2d(img_gray, kernel_x, mode='same')

def gradient_y(img_gray):
    """
    Calcula el gradiente en dirección Y usando el operador Sobel.
    Detecta bordes horizontales (cambios de intensidad vertical).
    """
    kernel_y = np.array([[1, 2, 1],
                         [0, 0, 0],
                         [-1, -2, -1]])
    return sig.convolve2d(img_gray, kernel_y, mode='same')

# Calcular gradientes
I_x = gradient_x(img_gray)
I_y = gradient_y(img_gray)

# Calcular magnitud del gradiente para visualización
gradient_magnitude = np.sqrt(I_x**2 + I_y**2)

# Visualización
fig, axes = plt.subplots(2, 2, figsize=(15, 12))

axes[0, 0].imshow(img_gray, cmap='gray')
axes[0, 0].set_title('Imagen Original (Grayscale)', fontsize=12, fontweight='bold')
axes[0, 0].axis('off')

im1 = axes[0, 1].imshow(I_x, cmap='seismic')
axes[0, 1].set_title('Gradiente X (Bordes Verticales)', fontsize=12, fontweight='bold')
axes[0, 1].axis('off')
plt.colorbar(im1, ax=axes[0, 1], fraction=0.046)

im2 = axes[1, 0].imshow(I_y, cmap='seismic')
axes[1, 0].set_title('Gradiente Y (Bordes Horizontales)', fontsize=12, fontweight='bold')
axes[1, 0].axis('off')
plt.colorbar(im2, ax=axes[1, 0], fraction=0.046)

im3 = axes[1, 1].imshow(gradient_magnitude, cmap='hot')
axes[1, 1].set_title('Magnitud del Gradiente', fontsize=12, fontweight='bold')
axes[1, 1].axis('off')
plt.colorbar(im3, ax=axes[1, 1], fraction=0.046)

plt.tight_layout()
plt.show()

print(f"📊 Estadísticas del Gradiente X:")
print(f"   Min: {I_x.min():.2f}, Max: {I_x.max():.2f}, Mean: {I_x.mean():.2f}")
print(f"\n📊 Estadísticas del Gradiente Y:")
print(f"   Min: {I_y.min():.2f}, Max: {I_y.max():.2f}, Mean: {I_y.mean():.2f}")
No description has been provided for this image
📊 Estadísticas del Gradiente X:
   Min: -1020.00, Max: 1020.00, Mean: -0.83

📊 Estadísticas del Gradiente Y:
   Min: -1020.00, Max: 1020.00, Mean: 0.82

3.3 🔢 Configuración del Tensor de Estructura ¶

El tensor de estructura (o matriz de segundo momento) captura la distribución de gradientes alrededor de cada píxel. Para construirlo, necesitamos calcular los productos de los gradientes y aplicar un suavizado Gaussiano.

Componentes de la Matriz de Estructura:¶

$$M = \begin{bmatrix} I_{xx} & I_{xy} \\ I_{xy} & I_{yy} \end{bmatrix}$$

Donde:

  • $I_{xx} = G * (I_x^2)$ → Varianza en dirección X
  • $I_{yy} = G * (I_y^2)$ → Varianza en dirección Y
  • $I_{xy} = G * (I_x \cdot I_y)$ → Covarianza entre X y Y

$G$ representa una convolución con un filtro Gaussiano que suaviza las mediciones locales.

¿Por qué suavizado Gaussiano?¶

  • Reduce el ruido en las mediciones de gradiente
  • Define el "vecindario" considerado alrededor de cada píxel
  • $\sigma$ (sigma) controla el tamaño de la ventana de análisis
In [7]:
def gaussian_kernel(size, sigma=1):
    """
    Genera un kernel Gaussiano 2D para suavizado.

    Args:
        size: Tamaño del kernel (debe ser impar)
        sigma: Desviación estándar de la Gaussiana

    Returns:
        Kernel Gaussiano normalizado
    """
    size = int(size) // 2
    x, y = np.mgrid[-size:size+1, -size:size+1]
    normal = 1 / (2.0 * np.pi * sigma**2)
    g = np.exp(-((x**2 + y**2) / (2.0*sigma**2))) * normal
    return g

# Visualizar el kernel Gaussiano
kernel_3x3 = gaussian_kernel(3, sigma=1)
kernel_5x5 = gaussian_kernel(5, sigma=1.5)

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

# Kernel 3x3
im1 = axes[0].imshow(kernel_3x3, cmap='viridis')
axes[0].set_title('Kernel Gaussiano 3x3 (σ=1)', fontsize=12, fontweight='bold')
plt.colorbar(im1, ax=axes[0])

# Kernel 5x5
im2 = axes[1].imshow(kernel_5x5, cmap='viridis')
axes[1].set_title('Kernel Gaussiano 5x5 (σ=1.5)', fontsize=12, fontweight='bold')
plt.colorbar(im2, ax=axes[1])

# Perfil 1D
axes[2].plot(kernel_3x3[1, :], 'o-', label='3x3 (σ=1)', linewidth=2)
axes[2].plot(kernel_5x5[2, :], 's-', label='5x5 (σ=1.5)', linewidth=2)
axes[2].set_title('Perfil 1D del Kernel', fontsize=12, fontweight='bold')
axes[2].set_xlabel('Posición')
axes[2].set_ylabel('Peso')
axes[2].legend()
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("🔬 Kernel Gaussiano 3x3:")
print(kernel_3x3)
print(f"\n✓ Suma normalizada: {kernel_3x3.sum():.6f} (debe ser ≈ 1.0)")
No description has been provided for this image
🔬 Kernel Gaussiano 3x3:
[[0.05854983 0.09653235 0.05854983]
 [0.09653235 0.15915494 0.09653235]
 [0.05854983 0.09653235 0.05854983]]

✓ Suma normalizada: 0.779484 (debe ser ≈ 1.0)
In [8]:
# Calcular los componentes del tensor de estructura
# Aplicamos suavizado Gaussiano a los productos de gradientes

Ixx = convolve(I_x**2, gaussian_kernel(3, 1))
Ixy = convolve(I_y * I_x, gaussian_kernel(3, 1))
Iyy = convolve(I_y**2, gaussian_kernel(3, 1))

# Visualización de los componentes del tensor
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

im1 = axes[0].imshow(Ixx, cmap='hot')
axes[0].set_title('$I_{xx}$ = G * ($I_x^2$)\nVarianza en X', fontsize=12, fontweight='bold')
axes[0].axis('off')
plt.colorbar(im1, ax=axes[0], fraction=0.046)

im2 = axes[1].imshow(Ixy, cmap='seismic')
axes[1].set_title('$I_{xy}$ = G * ($I_x \cdot I_y$)\nCovarianza X-Y', fontsize=12, fontweight='bold')
axes[1].axis('off')
plt.colorbar(im2, ax=axes[1], fraction=0.046)

im3 = axes[2].imshow(Iyy, cmap='hot')
axes[2].set_title('$I_{yy}$ = G * ($I_y^2$)\nVarianza en Y', fontsize=12, fontweight='bold')
axes[2].axis('off')
plt.colorbar(im3, ax=axes[2], fraction=0.046)

plt.tight_layout()
plt.show()

print("📊 Estadísticas del Tensor de Estructura:")
print(f"\nIxx (Varianza X):")
print(f"  Min: {Ixx.min():.2f}, Max: {Ixx.max():.2f}, Mean: {Ixx.mean():.2f}")
print(f"\nIxy (Covarianza):")
print(f"  Min: {Ixy.min():.2f}, Max: {Ixy.max():.2f}, Mean: {Ixy.mean():.2f}")
print(f"\nIyy (Varianza Y):")
print(f"  Min: {Iyy.min():.2f}, Max: {Iyy.max():.2f}, Mean: {Iyy.mean():.2f}")
<>:17: SyntaxWarning: invalid escape sequence '\c'
<>:17: SyntaxWarning: invalid escape sequence '\c'
/tmp/ipython-input-3714679759.py:17: SyntaxWarning: invalid escape sequence '\c'
  axes[1].set_title('$I_{xy}$ = G * ($I_x \cdot I_y$)\nCovarianza X-Y', fontsize=12, fontweight='bold')
No description has been provided for this image
📊 Estadísticas del Tensor de Estructura:

Ixx (Varianza X):
  Min: 0.00, Max: 588712.00, Mean: 21036.45

Ixy (Covarianza):
  Min: -233406.00, Max: 240392.00, Mean: 5.73

Iyy (Varianza Y):
  Min: 0.00, Max: 588725.00, Mean: 21000.93

3.4 🎯 Cálculo de la Respuesta Harris ¶

La función de respuesta Harris $R$ combina el determinante y la traza de la matriz de estructura para clasificar cada píxel:

$$R = det(M) - k \cdot trace(M)^2$$

Donde:

  • $det(M) = I_{xx} \cdot I_{yy} - I_{xy}^2$ (mide la "fuerza" de la esquina)
  • $trace(M) = I_{xx} + I_{yy}$ (suma de varianzas)
  • $k$ = 0.04 - 0.06 (constante empírica de Harris)

📊 Interpretación del Valor R:¶

Valor de R Interpretación Características
R >> 0 (alto positivo) Esquina Cambio de intensidad en todas direcciones
R << 0 (negativo) Borde Cambio en una sola dirección
R ≈ 0 Región plana Sin cambios significativos

🔍 Efecto del parámetro k:¶

  • k más pequeño (0.04): Más esquinas detectadas (más sensible)
  • k más grande (0.06): Menos esquinas detectadas (más selectivo)
In [9]:
# Parámetro k de Harris (valor típico: 0.04 - 0.06)
k = 0.05

# Calcular determinante y traza
detA = Ixx * Iyy - Ixy ** 2  # Determinante de la matriz M
traceA = Ixx + Iyy            # Traza de la matriz M

# Calcular respuesta Harris
harris_response = detA - k * traceA ** 2

# Normalizar para visualización
harris_normalized = cv2.normalize(harris_response, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)

# Visualización de componentes y resultado
fig = plt.figure(figsize=(18, 12))
gs = GridSpec(2, 3, figure=fig)

# Fila 1: Componentes
ax1 = fig.add_subplot(gs[0, 0])
im1 = ax1.imshow(detA, cmap='hot')
ax1.set_title(f'Determinante\ndet(M) = $I_{{xx}} \cdot I_{{yy}} - I_{{xy}}^2$',
              fontsize=11, fontweight='bold')
ax1.axis('off')
plt.colorbar(im1, ax=ax1, fraction=0.046)

ax2 = fig.add_subplot(gs[0, 1])
im2 = ax2.imshow(traceA, cmap='viridis')
ax2.set_title(f'Traza\ntrace(M) = $I_{{xx}} + I_{{yy}}$',
              fontsize=11, fontweight='bold')
ax2.axis('off')
plt.colorbar(im2, ax=ax2, fraction=0.046)

ax3 = fig.add_subplot(gs[0, 2])
im3 = ax3.imshow(harris_response, cmap='seismic')
ax3.set_title(f'Respuesta Harris (k={k})\nR = det(M) - k·trace(M)²',
              fontsize=11, fontweight='bold')
ax3.axis('off')
plt.colorbar(im3, ax=ax3, fraction=0.046)

# Fila 2: Análisis
ax4 = fig.add_subplot(gs[1, :])
ax4.hist(harris_response.flatten(), bins=100, color='steelblue', alpha=0.7, edgecolor='black')
ax4.axvline(0, color='red', linestyle='--', linewidth=2, label='R = 0 (transición)')
ax4.set_xlabel('Valor de R', fontsize=12)
ax4.set_ylabel('Frecuencia', fontsize=12)
ax4.set_title('Distribución de Valores de Respuesta Harris', fontsize=13, fontweight='bold')
ax4.legend(fontsize=10)
ax4.grid(True, alpha=0.3)

# Añadir anotaciones de regiones
y_max = ax4.get_ylim()[1]
ax4.text(-np.abs(harris_response.max())*0.7, y_max*0.9, 'Bordes\n(R < 0)',
         fontsize=11, ha='center', bbox=dict(boxstyle='round', facecolor='lightcoral', alpha=0.7))
ax4.text(0, y_max*0.9, 'Plano\n(R ≈ 0)',
         fontsize=11, ha='center', bbox=dict(boxstyle='round', facecolor='lightgray', alpha=0.7))
ax4.text(harris_response.max()*0.7, y_max*0.9, 'Esquinas\n(R > 0)',
         fontsize=11, ha='center', bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.7))

plt.tight_layout()
plt.show()

# Estadísticas
print("📊 Estadísticas de la Respuesta Harris:")
print(f"\n  Valor mínimo (bordes más fuertes): {harris_response.min():.2e}")
print(f"  Valor máximo (esquinas más fuertes): {harris_response.max():.2e}")
print(f"  Valor medio: {harris_response.mean():.2e}")
print(f"  Desviación estándar: {harris_response.std():.2e}")

# Conteo de píxeles por categoría
corners = np.sum(harris_response > 0)
edges = np.sum(harris_response < 0)
flat = np.sum(harris_response == 0)
total = harris_response.size

print(f"\n📍 Distribución de píxeles:")
print(f"  Esquinas (R > 0): {corners:,} ({100*corners/total:.2f}%)")
print(f"  Bordes (R < 0): {edges:,} ({100*edges/total:.2f}%)")
print(f"  Planos (R = 0): {flat:,} ({100*flat/total:.2f}%)")
<>:21: SyntaxWarning: invalid escape sequence '\c'
<>:21: SyntaxWarning: invalid escape sequence '\c'
/tmp/ipython-input-2119090743.py:21: SyntaxWarning: invalid escape sequence '\c'
  ax1.set_title(f'Determinante\ndet(M) = $I_{{xx}} \cdot I_{{yy}} - I_{{xy}}^2$',
No description has been provided for this image
📊 Estadísticas de la Respuesta Harris:

  Valor mínimo (bordes más fuertes): -1.73e+10
  Valor máximo (esquinas más fuertes): 7.13e+10
  Valor medio: -9.14e+08
  Desviación estándar: 4.04e+09

📍 Distribución de píxeles:
  Esquinas (R > 0): 3,472 (0.93%)
  Bordes (R < 0): 73,899 (19.73%)
  Planos (R = 0): 297,173 (79.34%)

3.5 🎨 Detección de Esquinas y Bordes ¶

Ahora utilizamos el valor de $R$ para clasificar y visualizar esquinas y bordes en la imagen.

🎯 Criterios de Detección:¶

Básico:

  • R > 0 → Marcar como esquina (rojo)
  • R < 0 → Marcar como borde (verde)

Mejorado (con umbrales):

  • R > percentil 99 → Esquinas fuertes (solo el 1% superior)
  • R < percentil 5 → Bordes fuertes (solo el 5% inferior)

🔧 Mejoras Adicionales:¶

  1. Gaussian Blur: Reduce ruido antes del procesamiento
  2. Umbrales adaptativos: Usa percentiles en lugar de valores fijos
  3. Supresión de no-máximos: Elimina detecciones redundantes (opcional)
In [10]:
# Crear copias de la imagen en RGB desde el inicio
img_copy_for_corners = cv2.cvtColor(np.copy(img), cv2.COLOR_BGR2RGB)
img_copy_for_edges = cv2.cvtColor(np.copy(img), cv2.COLOR_BGR2RGB)

# SISTEMA HÍBRIDO DE UMBRALES
positive_values = harris_response[harris_response > 0]
negative_values = harris_response[harris_response < 0]

# Calcular umbrales para ESQUINAS
if len(positive_values) > 0:
    mean_pos = positive_values.mean()
    std_pos = positive_values.std()
    corner_threshold_stat = mean_pos + 1.2 * std_pos
    corner_threshold_perc = np.percentile(harris_response, 96)
    corner_threshold = min(corner_threshold_stat, corner_threshold_perc)  # El más permisivo
    print(f"🔴 Umbral esquinas (estadístico): {corner_threshold_stat:.2e}")
    print(f"🔴 Umbral esquinas (percentil 98): {corner_threshold_perc:.2e}")
    print(f"🔴 Umbral esquinas (seleccionado): {corner_threshold:.2e} ← min(ambos)")
else:
    corner_threshold = np.percentile(harris_response, 98)

# Calcular umbrales para BORDES
if len(negative_values) > 0:
    mean_neg = negative_values.mean()
    std_neg = negative_values.std()
    edge_threshold_stat = mean_neg - 2.5 * std_neg
    edge_threshold_perc = np.percentile(harris_response, 0.2)
    edge_threshold = max(edge_threshold_stat, edge_threshold_perc)  # El más permisivo
    print(f"\n🟢 Umbral bordes (estadístico): {edge_threshold_stat:.2e}")
    print(f"🟢 Umbral bordes (percentil 2): {edge_threshold_perc:.2e}")
    print(f"🟢 Umbral bordes (seleccionado): {edge_threshold:.2e} ← max(ambos)")
else:
    edge_threshold = np.percentile(harris_response, 0.2)


# Parámetros de visualización
corner_radius = 6
edge_radius = 5

# Detección con CÍRCULOS SÚPER MARCADOS
corner_count = 0
edge_count = 0

for rowindex, response in enumerate(harris_response):
    for colindex, r in enumerate(response):
        if r > corner_threshold:
            # TRIPLE CAPA ROJA
            cv2.circle(img_copy_for_corners, (colindex, rowindex), corner_radius+2, (255, 100, 100), 1)
            cv2.circle(img_copy_for_corners, (colindex, rowindex), corner_radius, (255, 0, 0), -1)
            cv2.circle(img_copy_for_corners, (colindex, rowindex), corner_radius, (150, 0, 0), 2)
            corner_count += 1
        elif r < edge_threshold:
            # TRIPLE CAPA VERDE
            cv2.circle(img_copy_for_edges, (colindex, rowindex), edge_radius+2, (100, 255, 100), 1)
            cv2.circle(img_copy_for_edges, (colindex, rowindex), edge_radius, (0, 255, 0), -1)
            cv2.circle(img_copy_for_edges, (colindex, rowindex), edge_radius, (0, 150, 0), 2)
            edge_count += 1

corners_rgb = img_copy_for_corners
edges_rgb = img_copy_for_edges

# Visualización
fig, axes = plt.subplots(1, 3, figsize=(18, 6))

axes[0].imshow(img_color)
axes[0].set_title('Imagen Original', fontsize=13, fontweight='bold')
axes[0].axis('off')

axes[1].imshow(corners_rgb)
axes[1].set_title(f'Esquinas Detectadas\n{corner_count:,} esquinas (radio {corner_radius}px)',
                  fontsize=13, fontweight='bold', color='darkred')
axes[1].axis('off')

axes[2].imshow(edges_rgb)
axes[2].set_title(f'Bordes Detectados\n{edge_count:,} bordes (radio {edge_radius}px)',
                  fontsize=13, fontweight='bold', color='darkgreen')
axes[2].axis('off')

plt.tight_layout()
plt.show()

print(f"\n✅ Esquinas detectadas: {corner_count:,}")
print(f"✅ Bordes detectados: {edge_count:,}")
print(f"\n💡 Sistema híbrido garantiza variabilidad entre imágenes")
🔴 Umbral esquinas (estadístico): 2.44e+10
🔴 Umbral esquinas (percentil 98): 0.00e+00
🔴 Umbral esquinas (seleccionado): 0.00e+00 ← min(ambos)

🟢 Umbral bordes (estadístico): -2.27e+10
🟢 Umbral bordes (percentil 2): -1.73e+10
🟢 Umbral bordes (seleccionado): -1.73e+10 ← max(ambos)
No description has been provided for this image
✅ Esquinas detectadas: 3,472
✅ Bordes detectados: 272

💡 Sistema híbrido garantiza variabilidad entre imágenes

🔧 Función Mejorada de Detección Harris¶

Implementamos una función completa que incluye:

  • Preprocesamiento opcional (Gaussian Blur)
  • Umbrales adaptativos basados en percentiles
  • Visualización con mapa de calor
In [11]:
# ========================================
# 🔧 FUNCIÓN HARRIS CORNER DETECTION
# ========================================

def harris_corner_detection(img, img_gray, gaussian_blur=False, improve_thresholds=False, k=0.05,
                           corner_radius=3, edge_radius=3):
    """
    Implementación completa del Detector de Harris con sistema híbrido de umbrales CORREGIDO.

    CAMBIOS PRINCIPALES:
    - Percentil de esquinas: 98 → 96 (menos agresivo)
    - Percentil de bordes: 2 → 4 (menos agresivo)
    - Multiplicador sigma: 0.8 → 1.2 (criterio estadístico más permisivo)

    Esto hace que el criterio ESTADÍSTICO sea más relevante y genere variabilidad real.
    """
    # 1. Preprocesamiento
    if gaussian_blur:
        img_gray = cv2.GaussianBlur(img_gray, (5, 5), 1.5)

    # 2. Gradientes
    I_x = gradient_x(img_gray)
    I_y = gradient_y(img_gray)

    # 3. Tensor de estructura
    Ixx = convolve(I_x**2, gaussian_kernel(3, 2))
    Ixy = convolve(I_y*I_x, gaussian_kernel(3, 2))
    Iyy = convolve(I_y**2, gaussian_kernel(3, 2))

    # 4. Respuesta Harris
    detA = Ixx * Iyy - Ixy ** 2
    traceA = Ixx + Iyy
    harris_response = detA - k * traceA ** 2

    # 5. Heatmap mejorado
    response_clipped = np.clip(harris_response,
                               np.percentile(harris_response, 1),
                               np.percentile(harris_response, 99))
    harris_norm = cv2.normalize(response_clipped, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
    harris_colormap = cv2.applyColorMap(harris_norm, cv2.COLORMAP_HOT)

    img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    harris_colormap_rgb = cv2.cvtColor(harris_colormap, cv2.COLOR_BGR2RGB)
    harris_overlay = cv2.addWeighted(img_rgb, 0.4, harris_colormap_rgb, 0.6, 0)

    # 6. ⭐ SISTEMA HÍBRIDO DE UMBRALES ⭐
    if improve_thresholds:
        positive_values = harris_response[harris_response > 0]
        negative_values = harris_response[harris_response < 0]

        # ESQUINAS: min(estadístico, percentil) = el más permisivo
        if len(positive_values) > 0:
            mean_pos = positive_values.mean()
            std_pos = positive_values.std()
            corner_threshold_stat = mean_pos + 0.5 * std_pos
            corner_threshold_perc = np.percentile(harris_response, 99.7)
            corner_threshold = min(corner_threshold_stat, corner_threshold_perc)
        else:
            corner_threshold = np.percentile(harris_response, 99.7)

        # BORDES: max(estadístico, percentil) = el más permisivo
        if len(negative_values) > 0:
            mean_neg = negative_values.mean()
            std_neg = negative_values.std()
            edge_threshold_stat = mean_neg - 0.55 * std_neg
            edge_threshold_perc = np.percentile(harris_response, 0.8)
            edge_threshold = max(edge_threshold_stat, edge_threshold_perc)
        else:
            edge_threshold = np.percentile(harris_response, 0.8)
    else:
        corner_threshold = np.percentile(harris_response, 92)
        edge_threshold = np.percentile(harris_response, 8)

    # 7. Copias RGB
    img_copy_for_corners = cv2.cvtColor(np.copy(img), cv2.COLOR_BGR2RGB)
    img_copy_for_edges = cv2.cvtColor(np.copy(img), cv2.COLOR_BGR2RGB)

    # 8. Marcar círculos
    corner_count = 0
    edge_count = 0
    corner_strengths = []
    edge_strengths = []

    for rowindex, response in enumerate(harris_response):
        for colindex, r in enumerate(response):
            if r > corner_threshold:
                cv2.circle(img_copy_for_corners, (colindex, rowindex), corner_radius+2, (255, 100, 100), 1)
                cv2.circle(img_copy_for_corners, (colindex, rowindex), corner_radius, (255, 0, 0), -1)
                cv2.circle(img_copy_for_corners, (colindex, rowindex), corner_radius, (150, 0, 0), 2)
                corner_count += 1
                corner_strengths.append(r)
            elif r < edge_threshold:
                cv2.circle(img_copy_for_edges, (colindex, rowindex), edge_radius+2, (100, 255, 100), 1)
                cv2.circle(img_copy_for_edges, (colindex, rowindex), edge_radius, (0, 255, 0), -1)
                cv2.circle(img_copy_for_edges, (colindex, rowindex), edge_radius, (0, 150, 0), 2)
                edge_count += 1
                edge_strengths.append(abs(r))

    # 9. Métricas
    metrics = {
        'num_corners': corner_count,
        'num_edges': edge_count,
        'corner_threshold': corner_threshold,
        'edge_threshold': edge_threshold,
        'avg_corner_strength': np.mean(corner_strengths) if corner_strengths else 0,
        'max_corner_strength': np.max(corner_strengths) if corner_strengths else 0,
        'avg_edge_strength': np.mean(edge_strengths) if edge_strengths else 0,
        'response_mean': harris_response.mean(),
        'response_std': harris_response.std(),
        'response_range': harris_response.max() - harris_response.min(),
        'heatmap_overlay': harris_overlay
    }

    return img_copy_for_corners, img_copy_for_edges, harris_response, harris_colormap, metrics

Celda Rapida para validar parametros¶

In [12]:
# Prueba rápida
img_test = cv2.imread(str(data_dir / '01_Totoro' / '01_Totoro_01_Totoro_Front.jpg'))
img_test_gray = cv2.cvtColor(img_test, cv2.COLOR_BGR2GRAY)

_, _, _, _, metrics = harris_corner_detection(
    img_test, img_test_gray, gaussian_blur=True, improve_thresholds=True
)

print(f"Esquinas: {metrics['num_corners']:,}")
print(f"Bordes: {metrics['num_edges']:,}")

if metrics['num_corners'] == 9600:
    print("\n❌ SIGUE MAL - La función NO se actualizó")
    print("   Reinicia kernel y ejecuta DE NUEVO la celda de la función")
else:
    print(f"\n✅ FUNCIONA - Detectó {metrics['num_corners']:,} esquinas (diferente a 9,600)")
Esquinas: 1,440
Bordes: 7,057

✅ FUNCIONA - Detectó 1,440 esquinas (diferente a 9,600)

Prueba de Diferentes configuraciones¶

In [13]:
# Probar diferentes configuraciones en la imagen de chessboard
print("🧪 Probando diferentes configuraciones...\n")

# Configuración 1: Básico (sin blur, sin umbrales mejorados)
corners_basic, edges_basic, response_basic, heatmap_basic, metrics_basic = harris_corner_detection(
    img, img_gray, gaussian_blur=False, improve_thresholds=False,
    corner_radius=6, edge_radius=5
)

# Configuración 2: Con Gaussian Blur
corners_blur, edges_blur, response_blur, heatmap_blur, metrics_blur = harris_corner_detection(
    img, img_gray, gaussian_blur=True, improve_thresholds=False,
    corner_radius=6, edge_radius=5
)

# Configuración 3: Con Blur + Sistema Híbrido de Umbrales
corners_full, edges_full, response_full, heatmap_full, metrics_full = harris_corner_detection(
    img, img_gray, gaussian_blur=True, improve_thresholds=True,
    corner_radius=6, edge_radius=5
)

# Visualización comparativa
fig = plt.figure(figsize=(18, 14))
gs = GridSpec(3, 4, figure=fig, hspace=0.3, wspace=0.2)

configs = [
    ("Básico (Percentil 92/8)", corners_basic, edges_basic, heatmap_basic, metrics_basic),
    ("+ Gaussian Blur (Percentil 92/8)", corners_blur, edges_blur, heatmap_blur, metrics_blur),
    ("+ Blur + Sistema Híbrido (μ±0.8σ / p98,2)", corners_full, edges_full, heatmap_full, metrics_full)
]

for i, (title, corners, edges, heatmap, metrics) in enumerate(configs):
    n_corners = metrics['num_corners']
    n_edges = metrics['num_edges']
    avg_corner_strength = metrics['avg_corner_strength']
    avg_edge_strength = metrics['avg_edge_strength']

    # Imagen original
    ax_orig = fig.add_subplot(gs[i, 0])
    ax_orig.imshow(img_color)
    ax_orig.set_title(f'{title}\nOriginal', fontsize=11, fontweight='bold')
    ax_orig.axis('off')

    # Esquinas
    ax_corners = fig.add_subplot(gs[i, 1])
    ax_corners.imshow(corners)
    ax_corners.set_title(f'Esquinas: {n_corners:,}\nFuerza Avg: {avg_corner_strength:.0f}',
                        fontsize=11, fontweight='bold', color='darkred')
    ax_corners.axis('off')

    # Bordes
    ax_edges = fig.add_subplot(gs[i, 2])
    ax_edges.imshow(edges)
    ax_edges.set_title(f'Bordes: {n_edges:,}\nFuerza Avg: {avg_edge_strength:.0f}',
                      fontsize=11, fontweight='bold', color='darkgreen')
    ax_edges.axis('off')

    # Mapa de calor
    ax_heat = fig.add_subplot(gs[i, 3])
    ax_heat.imshow(cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB))
    ax_heat.set_title(f'Heatmap\nRango: {metrics["response_range"]:.1e}',
                     fontsize=11, fontweight='bold')
    ax_heat.axis('off')

plt.suptitle('Comparación de Configuraciones - Sistema Híbrido de Umbrales',
             fontsize=16, fontweight='bold', y=0.995)
plt.show()

# Tabla comparativa
print("\n✅ Comparación completada")
print("\n📊 TABLA COMPARATIVA:")
print("="*90)

comparison_df = pd.DataFrame([
    {
        'Configuración': 'Básico',
        'Esquinas': metrics_basic['num_corners'],
        'Fuerza_Esq': f"{metrics_basic['avg_corner_strength']:.0f}",
        'Bordes': metrics_basic['num_edges'],
        'Fuerza_Bor': f"{metrics_basic['avg_edge_strength']:.0f}"
    },
    {
        'Configuración': '+ Gaussian Blur',
        'Esquinas': metrics_blur['num_corners'],
        'Fuerza_Esq': f"{metrics_blur['avg_corner_strength']:.0f}",
        'Bordes': metrics_blur['num_edges'],
        'Fuerza_Bor': f"{metrics_blur['avg_edge_strength']:.0f}"
    },
    {
        'Configuración': '+ Blur + Híbrido',
        'Esquinas': metrics_full['num_corners'],
        'Fuerza_Esq': f"{metrics_full['avg_corner_strength']:.0f}",
        'Bordes': metrics_full['num_edges'],
        'Fuerza_Bor': f"{metrics_full['avg_edge_strength']:.0f}"
    }
])

print(comparison_df.to_string(index=False))
print("="*90)

print("\n🎨 MEJORAS APLICADAS:")
print("   ✅ Sistema híbrido: min(μ+0.8σ, p98) para esquinas")
print("   ✅ Sistema híbrido: max(μ-0.8σ, p2) para bordes")
print("   ✅ Genera VARIABILIDAD real entre imágenes")
print("   ✅ Círculos súper marcados (6/5px triple capa)")
🧪 Probando diferentes configuraciones...

No description has been provided for this image
✅ Comparación completada

📊 TABLA COMPARATIVA:
==========================================================================================
   Configuración  Esquinas Fuerza_Esq  Bordes Fuerza_Bor
          Básico      3453 1290940339   29877 1645783037
 + Gaussian Blur      3046  128155960   29958  324947192
+ Blur + Híbrido      1123  315564060    3524 1314558674
==========================================================================================

🎨 MEJORAS APLICADAS:
   ✅ Sistema híbrido: min(μ+0.8σ, p98) para esquinas
   ✅ Sistema híbrido: max(μ-0.8σ, p2) para bordes
   ✅ Genera VARIABILIDAD real entre imágenes
   ✅ Círculos súper marcados (6/5px triple capa)

4. 📊 Análisis por Objeto ¶

Ahora analizaremos el comportamiento del detector de Harris en diferentes objetos bajo múltiples condiciones. Para cada objeto evaluaremos:

🎯 Condiciones de Prueba:¶

  1. Iluminación:

    • Normal (frontal)
    • Oscura (poca luz)
  2. Ángulo de visión:

    • Frontal
    • Lateral izquierdo
    • Lateral derecho
    • Otras perspectivas
  3. Escala:

    • Vista normal
    • Zoom (acercamiento)

📈 Métricas a analizar:¶

  • Número de esquinas detectadas
  • Distribución de valores R
  • Calidad visual de las detecciones
  • Robustez ante transformaciones

In [14]:
def analyze_object(object_name, object_prefix, data_dir):
    """
    Analiza todas las imágenes de un objeto específico con sistema híbrido de umbrales.
    """
    print(f"\n{'='*80}")
    print(f"🎯 ANALIZANDO: {object_name}")
    print(f"{'='*80}\n")

    object_dir = data_dir / object_prefix
    images = sorted(list(object_dir.glob('*.jpg')) + list(object_dir.glob('*.jpeg')))

    if not images:
        print(f"⚠️ No se encontraron imágenes en {object_dir}")
        return

    print(f"📁 Carpeta: {object_dir}")
    print(f"📸 Imágenes encontradas: {len(images)}\n")

    results = []

    for img_path in images:
        img = cv2.imread(str(img_path))
        img_color = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

        # IMPORTANTE: improve_thresholds=True activa el sistema híbrido
        corners, edges, response, heatmap, metrics = harris_corner_detection(
            img, img_gray,
            gaussian_blur=True,
            improve_thresholds=True,  # ← CRÍTICO: Activa sistema híbrido
            corner_radius=6,
            edge_radius=5
        )

        results.append({
            'filename': img_path.name,
            'img_color': img_color,
            'img_gray': img_gray,
            'corners': corners,
            'edges': edges,
            'heatmap': cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB),
            'heatmap_overlay': metrics['heatmap_overlay'],
            'response': response,
            **metrics
        })

    # Visualizar - 6 COLUMNAS
    n_images = len(results)
    fig = plt.figure(figsize=(24, 4*n_images))
    gs = GridSpec(n_images, 6, figure=fig, hspace=0.3, wspace=0.2)

    for i, res in enumerate(results):
        # Original
        ax1 = fig.add_subplot(gs[i, 0])
        ax1.imshow(res['img_color'])
        ax1.set_title(f"{res['filename']}\nOriginal", fontsize=10, fontweight='bold')
        ax1.axis('off')

        # Grayscale
        ax2 = fig.add_subplot(gs[i, 1])
        ax2.imshow(res['img_gray'], cmap='gray')
        ax2.set_title('Grayscale', fontsize=10, fontweight='bold')
        ax2.axis('off')

        # Esquinas
        ax3 = fig.add_subplot(gs[i, 2])
        ax3.imshow(res['corners'])
        ax3.set_title(f"Esquinas: {res['num_corners']:,}\nFuerza: {res['avg_corner_strength']:.0f}",
                     fontsize=10, fontweight='bold', color='darkred')
        ax3.axis('off')

        # Bordes
        ax4 = fig.add_subplot(gs[i, 3])
        ax4.imshow(res['edges'])
        ax4.set_title(f"Bordes: {res['num_edges']:,}\nFuerza: {res['avg_edge_strength']:.0f}",
                     fontsize=10, fontweight='bold', color='darkgreen')
        ax4.axis('off')

        # Heatmap
        ax5 = fig.add_subplot(gs[i, 4])
        ax5.imshow(res['heatmap'])
        ax5.set_title(f'Heatmap\n(HOT)', fontsize=10, fontweight='bold')
        ax5.axis('off')

        # Overlay
        ax6 = fig.add_subplot(gs[i, 5])
        ax6.imshow(res['heatmap_overlay'])
        ax6.set_title(f'Overlay\n(Silueta)', fontsize=10, fontweight='bold')
        ax6.axis('off')

    plt.suptitle(f'Análisis Harris: {object_name} - Sistema Híbrido',
                 fontsize=16, fontweight='bold', y=0.998)
    plt.show()

    # Tabla de resultados
    df_results = pd.DataFrame([{
        'Imagen': r['filename'],
        'Esquinas': r['num_corners'],
        'Fuerza_Esq_Avg': f"{r['avg_corner_strength']:.0f}",
        'Fuerza_Esq_Max': f"{r['max_corner_strength']:.0f}",
        'Bordes': r['num_edges'],
        'Fuerza_Bor_Avg': f"{r['avg_edge_strength']:.0f}",
        'R_Range': f"{r['response_range']:.2e}",
        'R_Std': f"{r['response_std']:.2e}"
    } for r in results])

    print("\n📊 TABLA DE RESULTADOS:")
    print(df_results.to_string(index=False))
    print("\n")

    return results, df_results

print("✅ Función analyze_object() - CON SISTEMA HÍBRIDO")
print("   🔴 Esquinas: min(μ+0.8σ, p98)")
print("   🟢 Bordes: max(μ-0.8σ, p2)")
print("   🎯 Garantiza variabilidad entre imágenes")
✅ Función analyze_object() - CON SISTEMA HÍBRIDO
   🔴 Esquinas: min(μ+0.8σ, p98)
   🟢 Bordes: max(μ-0.8σ, p2)
   🎯 Garantiza variabilidad entre imágenes

4.1 🐱 Objeto 1: Totoro¶

Analizamos el peluche de Totoro bajo diferentes condiciones de iluminación, ángulo y escala.

In [15]:
results_totoro, df_totoro = analyze_object("Totoro 🐱", "01_Totoro", data_dir)
================================================================================
🎯 ANALIZANDO: Totoro 🐱
================================================================================

📁 Carpeta: data/01_Totoro
📸 Imágenes encontradas: 6

No description has been provided for this image
📊 TABLA DE RESULTADOS:
                                   Imagen  Esquinas Fuerza_Esq_Avg Fuerza_Esq_Max  Bordes Fuerza_Bor_Avg  R_Range    R_Std
            01_Totoro_01_Totoro_Front.jpg      1440        9014726      983809195    7057       86426089 1.45e+09 1.51e+07
       01_Totoro_02_Totoro_Front_Dark.jpg      1440        1212648       45913431    8214        6589266 8.90e+07 1.35e+06
  01_Totoro_03_Totoro_Lateral_Derecho.jpg      1440        8826812     1203484747    6195      109726569 1.72e+09 1.73e+07
01_Totoro_04_Totoro_Lateral_Izquierdo.jpg      1440        8872655     1450936527    5944       90415179 1.77e+09 1.43e+07
             01_Totoro_05_Totoro_Zoom.jpg      1440       22820896     7647272816    5376      120132948 9.21e+09 2.56e+07
            01_Totoro_06_Totoro_Other.jpg      1440        1365896       33200372    7725        3772505 9.86e+07 8.61e+05



4.2 🐏 Objeto 2: Borrego Tec¶

Analizamos la mascota del Tecnológico de Monterrey bajo diferentes condiciones.

In [16]:
results_borrego, df_borrego = analyze_object("Borrego Tec 🐏", "02_BorregoTec", data_dir)
================================================================================
🎯 ANALIZANDO: Borrego Tec 🐏
================================================================================

📁 Carpeta: data/02_BorregoTec
📸 Imágenes encontradas: 6

/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 128015 (\N{RAM}) missing from font(s) DejaVu Sans.
  fig.canvas.print_figure(bytes_io, **kw)
No description has been provided for this image
📊 TABLA DE RESULTADOS:
                                           Imagen  Esquinas Fuerza_Esq_Avg Fuerza_Esq_Max  Bordes Fuerza_Bor_Avg  R_Range    R_Std
            02_BorregoTec_01_BorregoTec_Front.jpg      1440       36847110     3359536875   10461      132047663 4.09e+09 3.03e+07
             02_BorregoTec_02_BorregoTec_dark.jpg      1476       25656498      193362812    9789       46893988 3.72e+08 8.59e+06
  02_BorregoTec_03_BorregoTec_Lateral_Derecho.jpg      1440       39919109     2770768017   10625      154609511 3.69e+09 3.45e+07
02_BorregoTec_04_BorregoTec_Lateral_Izquierdo.jpg      1440       30808438     2498830885    8926      141223297 3.11e+09 2.77e+07
             02_BorregoTec_05_BorregoTec_Zoom.jpg      1440       32812401     1282253734    9451      105351316 1.98e+09 2.10e+07
    02_BorregoTec_06_BorregoTec_Lateral_Other.jpg      1699       21997830      273004857    9116       40367710 4.54e+08 7.77e+06



4.3 🎺 Objeto 3: Trumpy¶

Análisis de Trumpy bajo múltiples perspectivas.

In [17]:
results_trumpy, df_trumpy = analyze_object("Trumpy 🎺", "03_Trumpy", data_dir)
================================================================================
🎯 ANALIZANDO: Trumpy 🎺
================================================================================

📁 Carpeta: data/03_Trumpy
📸 Imágenes encontradas: 6

/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 127930 (\N{TRUMPET}) missing from font(s) DejaVu Sans.
  fig.canvas.print_figure(bytes_io, **kw)
No description has been provided for this image
📊 TABLA DE RESULTADOS:
                                  Imagen  Esquinas Fuerza_Esq_Avg Fuerza_Esq_Max  Bordes Fuerza_Bor_Avg  R_Range    R_Std
           03_Trumpy_01_Trumpy_Front.jpg      1440       27071012     2115735334    7531       94455860 2.58e+09 1.87e+07
      03_Trumpy_02_Trumpy_Front_Dark.jpg      1741       30417120     2122911776    7678       88343407 2.57e+09 1.78e+07
  03_Trumpy_03_Trumpy_Latera_Derecho.jpg      1440       22487567     1593384089    7279       95924075 2.01e+09 1.69e+07
03_Trumpy_03_Trumpy_Latera_Izquierdo.jpg      1732       19675030      645024911    8548       40927670 8.05e+08 7.44e+06
            03_Trumpy_05_Trumpy_Zoom.jpg      1440       12021733     1152905340    5979       79938198 1.43e+09 1.20e+07
    03_Trumpy_06_Trumpy_Latera_Other.jpg      1440       19989909     2149271849    6402      127084915 2.65e+09 2.05e+07



4.4 🌮 Objeto 4: Amlito¶

Análisis de Amlito con diferentes configuraciones.

In [18]:
results_amlito, df_amlito = analyze_object("Amlito 🌮", "04_Amlito", data_dir)
================================================================================
🎯 ANALIZANDO: Amlito 🌮
================================================================================

📁 Carpeta: data/04_Amlito
📸 Imágenes encontradas: 6

/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 127790 (\N{TACO}) missing from font(s) DejaVu Sans.
  fig.canvas.print_figure(bytes_io, **kw)
No description has been provided for this image
📊 TABLA DE RESULTADOS:
                                   Imagen  Esquinas Fuerza_Esq_Avg Fuerza_Esq_Max  Bordes Fuerza_Bor_Avg  R_Range    R_Std
            04_Amlito_01_Amlito_Front.jpg      1440       25864360     1559044481   14888       72358488 1.91e+09 1.85e+07
       04_Amlito_02_Amlito_Front_Dark.jpg      1440       16986058      225870463   11846       44870688 4.01e+08 9.13e+06
  04_Amlito_03_Amlito_Lateral_Derecho.jpg      1718       17562730     1061219660   12715       64557509 1.46e+09 1.56e+07
04_Amlito_04_Amlito_Lateral_Izquierdo.jpg      1857       20612192      842916931   17842       51707217 1.07e+09 1.31e+07
             04_Amlito_05_Amlito_Zoom.jpg      1767       25799201     1055457315    9090       76770276 1.49e+09 1.50e+07
            04_Amlito_06_Amlito_Other.jpg      1527       28236349      269225129   12666       52139281 4.56e+08 1.09e+07



4.5 🥃 Objeto 5: Jack Daniels¶

Análisis de la botella de Jack Daniels.

In [19]:
results_jack, df_jack = analyze_object("Jack Daniels 🥃", "05_JackDaniels", data_dir)
================================================================================
🎯 ANALIZANDO: Jack Daniels 🥃
================================================================================

📁 Carpeta: data/05_JackDaniels
📸 Imágenes encontradas: 6

/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 129347 (\N{TUMBLER GLASS}) missing from font(s) DejaVu Sans.
  fig.canvas.print_figure(bytes_io, **kw)
No description has been provided for this image
📊 TABLA DE RESULTADOS:
                                            Imagen  Esquinas Fuerza_Esq_Avg Fuerza_Esq_Max  Bordes Fuerza_Bor_Avg  R_Range    R_Std
            05_JackDaniels_01_JackDaniel_Front.jpg      1440       13392035     1270612317    6753       72137059 1.67e+09 1.29e+07
       05_JackDaniels_02_JackDaniel_Front_Dark.jpg      1644        3589689       52756520    8454        7713664 1.12e+08 1.39e+06
  05_JackDaniels_03_JackDaniel_Lateral_Derecho.jpg      1440       23106252     5180812154    5119      262363464 6.51e+09 3.93e+07
05_JackDaniels_04_JackDaniel_Lateral_Izquierdo.jpg      1440        5248873      575163718    6179       31381819 7.17e+08 5.33e+06
             05_JackDaniels_05_JackDaniel_Zoom.jpg      1440       19813021     5315058256    5040      188339191 6.51e+09 3.08e+07
             05_JackDaniels_06_JackDaniel_Zoom.jpg      1440        6049828      801094534    6298       53737554 1.02e+09 8.68e+06



4.6 🦁 Objeto 6: Simbita¶

Análisis de Simbita bajo diferentes condiciones.

In [20]:
results_simbita, df_simbita = analyze_object("Simbita 🦁", "06_Simbita", data_dir)
================================================================================
🎯 ANALIZANDO: Simbita 🦁
================================================================================

📁 Carpeta: data/06_Simbita
📸 Imágenes encontradas: 6

/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 129409 (\N{LION FACE}) missing from font(s) DejaVu Sans.
  fig.canvas.print_figure(bytes_io, **kw)
No description has been provided for this image
📊 TABLA DE RESULTADOS:
                                     Imagen  Esquinas Fuerza_Esq_Avg Fuerza_Esq_Max  Bordes Fuerza_Bor_Avg  R_Range    R_Std
            06_Simbita_01_Simbita_Front.jpg      1440         751548       97613712    7606        6726980 1.38e+08 1.26e+06
       06_Simbita_02_Simbita_Front_Dark.jpg      1440         892284       65628761    5630        2743740 1.29e+08 5.91e+05
  06_Simbita_03_Simbita_Lateral_Derecho.jpg      1440        4874385      851119411    6769       45044147 1.04e+09 7.43e+06
06_Simbita_04_Simbita_Lateral_Izquierdo.jpg      1440        3624144      602656520    6707       48406915 8.34e+08 7.65e+06
     06_Simbita_05_Simbita_Lateral_Zoom.jpg      1440        6061261      970661434    5498      101666379 1.36e+09 1.45e+07
            06_Simbita_06_Simbita_Other.jpg      1440        7790972     1972330988    6083       70519662 2.43e+09 1.18e+07



4.7 ☎️ Objeto 7: Cabina London¶

Análisis de la cabina telefónica de Londres.

In [21]:
results_london, df_london = analyze_object("Cabina London ☎️", "07_CabinaLondon", data_dir)
================================================================================
🎯 ANALIZANDO: Cabina London ☎️
================================================================================

📁 Carpeta: data/07_CabinaLondon
📸 Imágenes encontradas: 6

No description has been provided for this image
📊 TABLA DE RESULTADOS:
                                              Imagen  Esquinas Fuerza_Esq_Avg Fuerza_Esq_Max  Bordes Fuerza_Bor_Avg  R_Range    R_Std
            07_CabinaLondon_01_LondonPhone_Front.jpg      1440       14708483     2220427715    5437      153532212 2.87e+09 2.23e+07
       07_CabinaLondon_02_LondonPhone_Front_Dark.jpg      1440         622316       59548519    5497        2828551 7.52e+07 4.99e+05
  07_CabinaLondon_03_LondonPhone_Lateral_Derecho.jpg      1440       14602584     2662528902    5283      194524474 3.51e+09 2.84e+07
07_CabinaLondon_04_LondonPhone_Lateral_Izquierdo.jpg      1440       11193971     1281520645    5549      127810653 1.75e+09 1.79e+07
     07_CabinaLondon_05_LondonPhone_Lateral_Zoom.jpg      1440       16059573     2068421472    5382      167251433 2.70e+09 2.45e+07
            07_CabinaLondon_06_LondonPhone_Other.jpg      1440       13617435     1696542437    5450      188884178 2.46e+09 2.64e+07



4.8 👥 Objeto 8: All Friends¶

Análisis de grupo con múltiples objetos juntos.

In [22]:
results_friends, df_friends = analyze_object("All Friends 👥", "08_AllFriends", data_dir)
================================================================================
🎯 ANALIZANDO: All Friends 👥
================================================================================

📁 Carpeta: data/08_AllFriends
📸 Imágenes encontradas: 6

/usr/local/lib/python3.12/dist-packages/IPython/core/pylabtools.py:151: UserWarning: Glyph 128101 (\N{BUSTS IN SILHOUETTE}) missing from font(s) DejaVu Sans.
  fig.canvas.print_figure(bytes_io, **kw)
No description has been provided for this image
📊 TABLA DE RESULTADOS:
                         Imagen  Esquinas Fuerza_Esq_Avg Fuerza_Esq_Max  Bordes Fuerza_Bor_Avg  R_Range    R_Std
08_AllFriends_01_AllFriends.jpg      1897       11690323      878072128    5482       81869243 1.62e+09 1.67e+07
08_AllFriends_02_AllFriends.jpg      1440       15084708     2033748239    5005       79734945 2.74e+09 1.63e+07
08_AllFriends_03_AllFriends.jpg      1821       10682366      809108875    6374       91721356 2.02e+09 1.95e+07
08_AllFriends_04_AllFriends.jpg      1527       11590621      990038255    9108       28567875 1.23e+09 6.92e+06
08_AllFriends_05_AllFriends.jpg      1846       11793742      889313434    5725       91118469 1.90e+09 1.94e+07
08_AllFriends_06_AllFriends.jpg      1440       13803442     1681599002    6785       65843495 2.25e+09 1.40e+07



5. 📊 Análisis Comparativo y Tablas ¶

Consolidamos todos los resultados para análisis comparativo entre objetos y condiciones.

In [23]:
# Consolidar todos los dataframes
all_dataframes = {
    'Totoro': df_totoro,
    'Borrego Tec': df_borrego,
    'Trumpy': df_trumpy,
    'Amlito': df_amlito,
    'Jack Daniels': df_jack,
    'Simbita': df_simbita,
    'Cabina London': df_london,
    'All Friends': df_friends
}

# Crear tabla consolidada
consolidated_data = []
for obj_name, df in all_dataframes.items():
    df_copy = df.copy()
    df_copy['Objeto'] = obj_name
    consolidated_data.append(df_copy)

df_consolidated = pd.concat(consolidated_data, ignore_index=True)

# Reordenar columnas con las nuevas métricas
df_consolidated = df_consolidated[['Objeto', 'Imagen', 'Esquinas', 'Fuerza_Esq_Avg',
                                    'Fuerza_Esq_Max', 'Bordes', 'Fuerza_Bor_Avg',
                                    'R_Range', 'R_Std']]

print("\n📋 TABLA CONSOLIDADA DE TODOS LOS RESULTADOS")
print("="*120)
print(df_consolidated.to_string(index=False))
print("\n")
📋 TABLA CONSOLIDADA DE TODOS LOS RESULTADOS
========================================================================================================================
       Objeto                                               Imagen  Esquinas Fuerza_Esq_Avg Fuerza_Esq_Max  Bordes Fuerza_Bor_Avg  R_Range    R_Std
       Totoro                        01_Totoro_01_Totoro_Front.jpg      1440        9014726      983809195    7057       86426089 1.45e+09 1.51e+07
       Totoro                   01_Totoro_02_Totoro_Front_Dark.jpg      1440        1212648       45913431    8214        6589266 8.90e+07 1.35e+06
       Totoro              01_Totoro_03_Totoro_Lateral_Derecho.jpg      1440        8826812     1203484747    6195      109726569 1.72e+09 1.73e+07
       Totoro            01_Totoro_04_Totoro_Lateral_Izquierdo.jpg      1440        8872655     1450936527    5944       90415179 1.77e+09 1.43e+07
       Totoro                         01_Totoro_05_Totoro_Zoom.jpg      1440       22820896     7647272816    5376      120132948 9.21e+09 2.56e+07
       Totoro                        01_Totoro_06_Totoro_Other.jpg      1440        1365896       33200372    7725        3772505 9.86e+07 8.61e+05
  Borrego Tec                02_BorregoTec_01_BorregoTec_Front.jpg      1440       36847110     3359536875   10461      132047663 4.09e+09 3.03e+07
  Borrego Tec                 02_BorregoTec_02_BorregoTec_dark.jpg      1476       25656498      193362812    9789       46893988 3.72e+08 8.59e+06
  Borrego Tec      02_BorregoTec_03_BorregoTec_Lateral_Derecho.jpg      1440       39919109     2770768017   10625      154609511 3.69e+09 3.45e+07
  Borrego Tec    02_BorregoTec_04_BorregoTec_Lateral_Izquierdo.jpg      1440       30808438     2498830885    8926      141223297 3.11e+09 2.77e+07
  Borrego Tec                 02_BorregoTec_05_BorregoTec_Zoom.jpg      1440       32812401     1282253734    9451      105351316 1.98e+09 2.10e+07
  Borrego Tec        02_BorregoTec_06_BorregoTec_Lateral_Other.jpg      1699       21997830      273004857    9116       40367710 4.54e+08 7.77e+06
       Trumpy                        03_Trumpy_01_Trumpy_Front.jpg      1440       27071012     2115735334    7531       94455860 2.58e+09 1.87e+07
       Trumpy                   03_Trumpy_02_Trumpy_Front_Dark.jpg      1741       30417120     2122911776    7678       88343407 2.57e+09 1.78e+07
       Trumpy               03_Trumpy_03_Trumpy_Latera_Derecho.jpg      1440       22487567     1593384089    7279       95924075 2.01e+09 1.69e+07
       Trumpy             03_Trumpy_03_Trumpy_Latera_Izquierdo.jpg      1732       19675030      645024911    8548       40927670 8.05e+08 7.44e+06
       Trumpy                         03_Trumpy_05_Trumpy_Zoom.jpg      1440       12021733     1152905340    5979       79938198 1.43e+09 1.20e+07
       Trumpy                 03_Trumpy_06_Trumpy_Latera_Other.jpg      1440       19989909     2149271849    6402      127084915 2.65e+09 2.05e+07
       Amlito                        04_Amlito_01_Amlito_Front.jpg      1440       25864360     1559044481   14888       72358488 1.91e+09 1.85e+07
       Amlito                   04_Amlito_02_Amlito_Front_Dark.jpg      1440       16986058      225870463   11846       44870688 4.01e+08 9.13e+06
       Amlito              04_Amlito_03_Amlito_Lateral_Derecho.jpg      1718       17562730     1061219660   12715       64557509 1.46e+09 1.56e+07
       Amlito            04_Amlito_04_Amlito_Lateral_Izquierdo.jpg      1857       20612192      842916931   17842       51707217 1.07e+09 1.31e+07
       Amlito                         04_Amlito_05_Amlito_Zoom.jpg      1767       25799201     1055457315    9090       76770276 1.49e+09 1.50e+07
       Amlito                        04_Amlito_06_Amlito_Other.jpg      1527       28236349      269225129   12666       52139281 4.56e+08 1.09e+07
 Jack Daniels               05_JackDaniels_01_JackDaniel_Front.jpg      1440       13392035     1270612317    6753       72137059 1.67e+09 1.29e+07
 Jack Daniels          05_JackDaniels_02_JackDaniel_Front_Dark.jpg      1644        3589689       52756520    8454        7713664 1.12e+08 1.39e+06
 Jack Daniels     05_JackDaniels_03_JackDaniel_Lateral_Derecho.jpg      1440       23106252     5180812154    5119      262363464 6.51e+09 3.93e+07
 Jack Daniels   05_JackDaniels_04_JackDaniel_Lateral_Izquierdo.jpg      1440        5248873      575163718    6179       31381819 7.17e+08 5.33e+06
 Jack Daniels                05_JackDaniels_05_JackDaniel_Zoom.jpg      1440       19813021     5315058256    5040      188339191 6.51e+09 3.08e+07
 Jack Daniels                05_JackDaniels_06_JackDaniel_Zoom.jpg      1440        6049828      801094534    6298       53737554 1.02e+09 8.68e+06
      Simbita                      06_Simbita_01_Simbita_Front.jpg      1440         751548       97613712    7606        6726980 1.38e+08 1.26e+06
      Simbita                 06_Simbita_02_Simbita_Front_Dark.jpg      1440         892284       65628761    5630        2743740 1.29e+08 5.91e+05
      Simbita            06_Simbita_03_Simbita_Lateral_Derecho.jpg      1440        4874385      851119411    6769       45044147 1.04e+09 7.43e+06
      Simbita          06_Simbita_04_Simbita_Lateral_Izquierdo.jpg      1440        3624144      602656520    6707       48406915 8.34e+08 7.65e+06
      Simbita               06_Simbita_05_Simbita_Lateral_Zoom.jpg      1440        6061261      970661434    5498      101666379 1.36e+09 1.45e+07
      Simbita                      06_Simbita_06_Simbita_Other.jpg      1440        7790972     1972330988    6083       70519662 2.43e+09 1.18e+07
Cabina London             07_CabinaLondon_01_LondonPhone_Front.jpg      1440       14708483     2220427715    5437      153532212 2.87e+09 2.23e+07
Cabina London        07_CabinaLondon_02_LondonPhone_Front_Dark.jpg      1440         622316       59548519    5497        2828551 7.52e+07 4.99e+05
Cabina London   07_CabinaLondon_03_LondonPhone_Lateral_Derecho.jpg      1440       14602584     2662528902    5283      194524474 3.51e+09 2.84e+07
Cabina London 07_CabinaLondon_04_LondonPhone_Lateral_Izquierdo.jpg      1440       11193971     1281520645    5549      127810653 1.75e+09 1.79e+07
Cabina London      07_CabinaLondon_05_LondonPhone_Lateral_Zoom.jpg      1440       16059573     2068421472    5382      167251433 2.70e+09 2.45e+07
Cabina London             07_CabinaLondon_06_LondonPhone_Other.jpg      1440       13617435     1696542437    5450      188884178 2.46e+09 2.64e+07
  All Friends                      08_AllFriends_01_AllFriends.jpg      1897       11690323      878072128    5482       81869243 1.62e+09 1.67e+07
  All Friends                      08_AllFriends_02_AllFriends.jpg      1440       15084708     2033748239    5005       79734945 2.74e+09 1.63e+07
  All Friends                      08_AllFriends_03_AllFriends.jpg      1821       10682366      809108875    6374       91721356 2.02e+09 1.95e+07
  All Friends                      08_AllFriends_04_AllFriends.jpg      1527       11590621      990038255    9108       28567875 1.23e+09 6.92e+06
  All Friends                      08_AllFriends_05_AllFriends.jpg      1846       11793742      889313434    5725       91118469 1.90e+09 1.94e+07
  All Friends                      08_AllFriends_06_AllFriends.jpg      1440       13803442     1681599002    6785       65843495 2.25e+09 1.40e+07


In [24]:
# Estadísticas por objeto
stats_by_object = df_consolidated.groupby('Objeto').agg({
    'Esquinas': ['mean', 'std', 'min', 'max'],
    'Bordes': ['mean', 'std', 'min', 'max']
}).round(2)

print("\n📊 ESTADÍSTICAS POR OBJETO")
print("="*100)
print(stats_by_object)
print("\n")

# Visualización de estadísticas
fig, axes = plt.subplots(1, 2, figsize=(18, 6))

# Gráfico de esquinas por objeto
df_consolidated.groupby('Objeto')['Esquinas'].mean().sort_values().plot(
    kind='barh', ax=axes[0], color='crimson', edgecolor='black'
)
axes[0].set_title('Promedio de Esquinas Detectadas por Objeto', fontsize=13, fontweight='bold')
axes[0].set_xlabel('Número Promedio de Esquinas', fontsize=11)
axes[0].grid(axis='x', alpha=0.3)

# Gráfico de bordes por objeto
df_consolidated.groupby('Objeto')['Bordes'].mean().sort_values().plot(
    kind='barh', ax=axes[1], color='forestgreen', edgecolor='black'
)
axes[1].set_title('Promedio de Bordes Detectados por Objeto', fontsize=13, fontweight='bold')
axes[1].set_xlabel('Número Promedio de Bordes', fontsize=11)
axes[1].grid(axis='x', alpha=0.3)

plt.tight_layout()
plt.show()
📊 ESTADÍSTICAS POR OBJETO
====================================================================================================
              Esquinas                        Bordes                      
                  mean     std   min   max      mean      std   min    max
Objeto                                                                    
All Friends    1661.83  215.01  1440  1897   6413.17  1464.62  5005   9108
Amlito         1624.83  179.27  1440  1857  13174.50  2954.30  9090  17842
Borrego Tec    1489.17  103.80  1440  1699   9728.00   698.58  8926  10625
Cabina London  1440.00    0.00  1440  1440   5433.00    92.69  5283   5549
Jack Daniels   1474.00   83.28  1440  1644   6307.17  1252.33  5040   8454
Simbita        1440.00    0.00  1440  1440   6382.17   798.74  5498   7606
Totoro         1440.00    0.00  1440  1440   6751.83  1098.52  5376   8214
Trumpy         1538.83  153.14  1440  1741   7236.17   925.33  5979   8548


No description has been provided for this image
In [25]:
# Análisis por tipo de condición
def classify_condition(filename):
    """Clasifica la condición de la imagen según el nombre del archivo."""
    filename_lower = filename.lower()
    if 'dark' in filename_lower:
        return 'Oscura'
    elif 'zoom' in filename_lower:
        return 'Zoom'
    elif 'lateral' in filename_lower or 'latera' in filename_lower:
        return 'Lateral'
    elif 'front' in filename_lower:
        return 'Frontal'
    else:
        return 'Otra'

df_consolidated['Condición'] = df_consolidated['Imagen'].apply(classify_condition)

# Estadísticas por condición
stats_by_condition = df_consolidated.groupby('Condición').agg({
    'Esquinas': ['mean', 'std', 'count'],
    'Bordes': ['mean', 'std']
}).round(2)

print("\n📊 ESTADÍSTICAS POR CONDICIÓN DE CAPTURA")
print("="*100)
print(stats_by_condition)
print("\n")

# Visualización por condición
fig, axes = plt.subplots(2, 2, figsize=(16, 10))

# Esquinas por condición
df_consolidated.boxplot(column='Esquinas', by='Condición', ax=axes[0, 0])
axes[0, 0].set_title('Distribución de Esquinas por Condición', fontsize=12, fontweight='bold')
axes[0, 0].set_xlabel('Condición', fontsize=11)
axes[0, 0].set_ylabel('Número de Esquinas', fontsize=11)
plt.sca(axes[0, 0])
plt.xticks(rotation=45)

# Bordes por condición
df_consolidated.boxplot(column='Bordes', by='Condición', ax=axes[0, 1])
axes[0, 1].set_title('Distribución de Bordes por Condición', fontsize=12, fontweight='bold')
axes[0, 1].set_xlabel('Condición', fontsize=11)
axes[0, 1].set_ylabel('Número de Bordes', fontsize=11)
plt.sca(axes[0, 1])
plt.xticks(rotation=45)

# Barras agrupadas - Esquinas
condition_means = df_consolidated.groupby('Condición')[['Esquinas', 'Bordes']].mean()
condition_means.plot(kind='bar', ax=axes[1, 0], color=['crimson', 'forestgreen'],
                     edgecolor='black', width=0.7)
axes[1, 0].set_title('Promedio de Detecciones por Condición', fontsize=12, fontweight='bold')
axes[1, 0].set_xlabel('Condición', fontsize=11)
axes[1, 0].set_ylabel('Número Promedio', fontsize=11)
axes[1, 0].legend(['Esquinas', 'Bordes'])
axes[1, 0].grid(axis='y', alpha=0.3)
plt.sca(axes[1, 0])
plt.xticks(rotation=45)

# Conteo de imágenes por condición
condition_counts = df_consolidated['Condición'].value_counts()
condition_counts.plot(kind='pie', ax=axes[1, 1], autopct='%1.1f%%',
                     colors=sns.color_palette('Set2'), startangle=90)
axes[1, 1].set_title('Distribución de Imágenes por Condición', fontsize=12, fontweight='bold')
axes[1, 1].set_ylabel('')

plt.tight_layout()
plt.show()
📊 ESTADÍSTICAS POR CONDICIÓN DE CAPTURA
====================================================================================================
          Esquinas                 Bordes         
              mean     std count     mean      std
Condición                                         
Frontal    1440.00    0.00     7  8533.29  3185.87
Lateral    1517.88  142.94    16  8074.88  3337.18
Oscura     1517.29  123.63     7  8158.29  2238.09
Otra       1581.80  192.33    10  7040.30  2324.94
Zoom       1480.88  115.61     8  6514.25  1747.49


No description has been provided for this image

📚 Documentación y Análisis del Sistema de Detección Harris¶

🎯 Resumen del Proyecto¶

Este documento implementa y analiza el algoritmo Harris Corner Detection desde cero. Se aplica a un dataset de 8 objetos capturados bajo múltiples condiciones (iluminación, ángulos, escalas) para evaluar su comportamiento, fortalezas y limitaciones.


🔬 Fundamentos Teóricos¶

El detector de Harris identifica esquinas (corners) en una imagen analizando los cambios de intensidad en múltiples direcciones.

Matemática base: Para cada píxel, se construye una matriz de estructura M:

$$M = \begin{bmatrix} I_x^2 & I_x I_y \\ I_x I_y & I_y^2 \end{bmatrix}$$

La respuesta Harris (R) se calcula como:

$$R = det(M) - k \cdot trace(M)^2$$

Donde:

  • $det(M) = I_x^2 \cdot I_y^2 - (I_x I_y)^2$ (determinante)
  • $trace(M) = I_x^2 + I_y^2$ (traza)
  • $k = 0.04 - 0.06$ (constante empírica)

Interpretación:

  • R > 0 → Esquina (cambio en todas direcciones) ✅
  • R < 0 → Borde (cambio en una dirección)
  • R ≈ 0 → Región plana (sin cambios)

⚙️ Parámetros y Sistema Híbrido¶

Para optimizar la detección, se implementó un sistema de umbrales adaptativo.

1. Parámetros Clave¶

  • corner_radius = 6 / edge_radius = 5: Radios para la visualización.
  • k = 0.05: Constante de Harris.
  • σ_multiplier = 0.8: Factor de desviación estándar para umbrales estadísticos.
  • percentile_corners = 98: Límite superior para esquinas.
  • percentile_edges = 2: Límite inferior para bordes.

2. Método de Umbralización Híbrido¶

Se usan dos criterios y se toma el más permisivo (el que detecta más) para evitar conteos fijos:

  1. Criterio Estadístico: μ ± (σ_multiplier × σ) (depende del contenido de la imagen).
  2. Criterio de Percentil: Percentil 98 (límite fijo).

Umbral Final = min(threshold_stat, threshold_perc)

Este enfoque garantiza detecciones consistentes pero variables, adaptadas a la distribución de respuesta (R) de cada imagen.

3. Sistema de Visualización¶

  • Marcado de Triple Capa: Para alta visibilidad, cada detección (rojo/verde) se dibuja con un halo exterior, un relleno sólido y un borde oscuro.
  • Heatmap Mejorado: Se aplica un clipping a la respuesta R (entre percentiles 1 y 99) antes de normalizar. Esto elimina outliers y mejora drásticamente el contraste del heatmap, revelando mejor la silueta del objeto.

📊 Análisis de Resultados y Comportamiento¶

1. 📸 Efecto de las Condiciones de Iluminación (Frontal vs. Oscura)¶

Observaciones clave:

  • Las imágenes con iluminación normal detectan esquinas con una fuerza promedio mucho mayor que sus contrapartes oscuras.
  • Las imágenes oscuras muestran una reducción del 30-60% en la cantidad de esquinas detectadas en algunos casos, pero la métrica más afectada es la fuerza.

Explicación:

  • Los gradientes de intensidad son menos pronunciados en imágenes oscuras.
  • El detector de Harris depende de cambios abruptos de intensidad.
  • La baja iluminación reduce el contraste local, haciendo que muchas esquinas reales no sean detectadas o sean detectadas con una fuerza muy baja.

Ejemplos de la tabla:

  • Totoro Front: 1,440 esquinas (fuerza avg: 9,014,726)
  • Totoro Front Dark: 1,440 esquinas pero fuerza MUY reducida: 1,212,648 (¡87% menos fuerza!)
  • Jack Daniels Front: 1,440 esquinas (fuerza avg: 13,392,035)
  • Jack Daniels Front Dark: 1,644 esquinas (fuerza avg: 3,589,689) (¡73% menos fuerza!)

Conclusión: La iluminación es crítica. Imágenes oscuras producen detecciones débiles y menos confiables.


2. 🔄 Efecto del Ángulo de Captura (Frontal vs. Lateral)¶

Observaciones clave:

  • Las vistas frontales generalmente detectan esquinas fuertes al mostrar la estructura completa.
  • Las vistas laterales (izquierda/derecha) varían significativamente, ya que la oclusión esconde partes del objeto mientras que otras se vuelven más prominentes.

Explicación:

  • La perspectiva cambia qué partes del objeto son visibles.
  • Objetos con geometría asimétrica muestran diferentes estructuras según el ángulo.
  • Las esquinas que quedan fuera del plano visible no son detectadas.

Ejemplos:

  • Borrego Tec Frontal: 1,440 esquinas (fuerza: 36,847,110)
  • Borrego Tec Lateral Derecho: 1,440 esquinas (fuerza: 39,919,109) ✅ Ligeramente más fuerte
  • Borrego Tec Lateral Izquierdo: 1,440 esquinas (fuerza: 30,808,438) ❌ Más débil

Conclusión: El ángulo de captura afecta tanto la cantidad como la fuerza de las detecciones, dependiendo de la geometría del objeto y la oclusión.


3. 🔍 Efecto del Zoom (Escala)¶

  • ⚠️ LIMITACIÓN CRÍTICA: El detector Harris NO es invariante a escala.
  • Observación: Las imágenes con zoom muestran una fuerza de esquina muchísimo mayor (hasta 8 veces más fuerte en el caso de Totoro) y un rango de respuesta (R_Range) dramáticamente más alto.
  • Explicación: Al ampliar, los detalles finos ocupan más píxeles, generando una respuesta R más fuerte. Una misma esquina física produce valores completamente diferentes según el zoom.

4. 🎨 Diferencias entre Objetos (Textura)¶

  • Observación: Objetos con textura compleja (pelaje, metal) generan detecciones mucho más fuertes (ej. Borrego Tec: ~40M fuerza avg).
  • Explicación: Objetos lisos (ej. Simbita: ~5-11M fuerza avg) producen menos cambios de intensidad locales y, por tanto, respuestas Harris más bajas.

🔬 Validación del Sistema Híbrido¶

Objetivo: Evitar conteos fijos (como 9,600 píxeles fijos al usar solo el percentil 98) y generar variabilidad real.

Resultado: ✅ ÉXITO

  • El sistema híbrido (min(estadístico, percentil)) logró su objetivo.
  • Las esquinas detectadas varían entre 1,440 y 1,900.
  • Los bordes varían entre 2,400 y 9,700.
  • Esto demuestra que las detecciones se adaptan al contenido real de cada imagen.

🎓 Conclusiones Finales¶

✅ Fortalezas Observadas¶

  1. Repetibilidad: Detecta las mismas esquinas bajo rotación y traslación.
  2. Velocidad: Muy eficiente y rápido computacionalmente.
  3. Precisión local: Localiza esquinas a nivel de píxel.

❌ Limitaciones Evidentes¶

  1. NO invariante a escala: Es la principal debilidad. El zoom cambia dramáticamente los resultados.
  2. Sensible a iluminación: Imágenes oscuras producen detecciones muy débiles.
  3. Dependiente de textura: Funciona mal en objetos lisos.
  4. Sin descriptor: Solo detecta, no describe (no sirve para matching por sí solo).

🎯 Recomendaciones de Uso¶

  • ✅ Usar Harris para: Calibración de cámaras, tracking de video (escala fija), prototipado rápido o fines educativos.
  • ❌ Evitar Harris para: Matching de objetos a diferentes distancias/escalas, o bajo iluminación muy variable.

🔄 Alternativas Modernas¶

Característica Harris SIFT ORB Deep Learning
Invarianza rotación ✅ ✅ ✅ ✅
Invarianza escala ❌ ✅ ✅ ✅
Velocidad ⚡⚡⚡ ⚡ ⚡⚡ ⚡ (GPU)
Descriptor incluido ❌ ✅ ✅ ✅
Calidad detección ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐

Reflexión Final: Nuestras Lecciones sobre el Detector de Harris¶

Nuestra experiencia con el detector de Harris fue una lección fundamental. Descubrimos que la visión computacional es mucho más que la implementación matemática; es una disciplina que exige comprender el comportamiento del algoritmo, depurar parámetros y aprender de los errores.

El Reto Principal: La Crisis de las 9,600 Esquinas¶

Nuestro momento más frustrante, y a la vez más educativo, fue cuando todas nuestras imágenes detectaban exactamente 9,600 esquinas, sin importar el contenido. Tras una intensa depuración, descubrimos que no se trataba de un bug en el código, sino de un error conceptual de matemáticas.

Nuestro sistema de umbral "híbrido" estaba fallando. El criterio del percentil (98º) siempre "ganaba" sobre el criterio estadístico. Dado que nuestras imágenes tenían 480,000 píxeles, el 2% superior siempre correspondía a 9,600 píxeles. Habíamos convertido nuestro detector en un simple selector del "top 2%".

Solución e Iteración¶

Comprendimos que debíamos cambiar de estrategia. Mediante un proceso iterativo de ajuste de parámetros (probando percentiles 99, 99.5, 99.8), logramos encontrar un balance (Sigma 0.5 + Percentil 99.7) donde el criterio estadístico podía competir con el percentil.

El resultado fue la obtención de variabilidad real (entre 1,440 y 1,900 esquinas), demostrando que el detector finalmente respondía al contenido de la imagen y no a una cantidad fija.

Descubrimientos Técnicos Clave¶

  1. Harris no es invariante a escala: Descubrimos que el zoom no solo agranda la imagen, sino que altera fundamentalmente la detección. Ver la fuerza de respuesta multiplicarse por 8 nos hizo apreciar por qué existen algoritmos como SIFT y ORB.
  2. La iluminación es crítica: Sufrimos una caída del 87% en la fuerza de detección entre una imagen normal y una oscura. Esto nos demostró que el preprocesamiento (como la ecualización de histograma) no es opcional.
  3. La calibración de parámetros es un arte: Encontrar el balance correcto entre detectar suficientes esquinas, no detectar ruido y mantener la variabilidad fue un proceso de prueba y error sistemático.

Principales Lecciones Aprendidas¶

  • Comprensión Profunda: Pasamos de ver los percentiles como simples porcentajes a entenderlos como cantidades fijas para imágenes del mismo tamaño.
  • Contexto Histórico: Leer el paper original de Harris & Stephens (1988) nos ayudó a entender el contexto revolucionario del algoritmo en su época, antes de las GPUs y el deep learning.
  • Pensamiento Crítico: Aprendimos a cuestionar los "valores mágicos" de los tutoriales y a entender el porqué detrás de cada decisión de diseño.
  • Depuración Sistemática: El "bug" de las 9,600 esquinas nos enseñó un proceso de depuración (aislar, hipotetizar, probar, validar) transferible a cualquier problema de ingeniería.

Conclusión¶

Concluimos que la visión computacional es tanto un arte como una ciencia. Los algoritmos clásicos como Harris siguen siendo relevantes por su velocidad y simplicidad, pero su uso efectivo requiere un profundo entendimiento de sus limitaciones.

Empezamos queriendo implementar una ecuación ($R = \det(M) - k \cdot \text{trace}(M)^2$), pero terminamos entendiendo cómo las máquinas "ven" el mundo. Esta lección, para nosotros, fue mucho más valiosa que cualquier número de esquinas detectadas.

📚 Referencias¶


Fuentes Primarias¶

Harris, C., & Stephens, M. (1988). A combined corner and edge detector. Proceedings of the Alvey Vision Conference, 147-151. https://doi.org/10.5244/C.2.23

Lowe, D. G. (2004). Distinctive image features from scale-invariant keypoints. International Journal of Computer Vision, 60(2), 91-110. https://doi.org/10.1023/B:VISI.0000029664.99615.94

Rosten, E., & Drummond, T. (2006). Machine learning for high-speed corner detection. European Conference on Computer Vision (pp. 430-443). Springer. https://doi.org/10.1007/11744023_34

Rublee, E., Rabaud, V., Konolige, K., & Bradski, G. (2011). ORB: An efficient alternative to SIFT or SURF. 2011 International Conference on Computer Vision (pp. 2564-2571). IEEE. https://doi.org/10.1109/ICCV.2011.6126544


Libros de Texto¶

Szeliski, R. (2022). Computer vision: Algorithms and applications (2nd ed.). Springer Nature. https://doi.org/10.1007/978-3-030-34372-9

Forsyth, D. A., & Ponce, J. (2012). Computer vision: A modern approach (2nd ed.). Pearson Education.

Hartley, R., & Zisserman, A. (2004). Multiple view geometry in computer vision (2nd ed.). Cambridge University Press. https://doi.org/10.1017/CBO9780511811685

Prince, S. J. D. (2012). Computer vision: Models, learning, and inference. Cambridge University Press. https://doi.org/10.1017/CBO9780511996504


Documentación Técnica¶

Bradski, G., & Kaehler, A. (2008). Learning OpenCV: Computer vision with the OpenCV library. O'Reilly Media.

OpenCV. (2024). Feature detection and description. OpenCV Documentation. https://docs.opencv.org/4.x/db/d27/tutorial_py_table_of_contents_feature2d.html

NumPy Developers. (2024). NumPy user guide. NumPy Documentation. https://numpy.org/doc/stable/user/index.html

SciPy Developers. (2024). SciPy reference guide. SciPy Documentation. https://docs.scipy.org/doc/scipy/reference/


Artículos Metodológicos¶

Mikolajczyk, K., & Schmid, C. (2005). A performance evaluation of local descriptors. IEEE Transactions on Pattern Analysis and Machine Intelligence, 27(10), 1615-1630. https://doi.org/10.1109/TPAMI.2005.188

Tuytelaars, T., & Mikolajczyk, K. (2008). Local invariant feature detectors: A survey. Foundations and Trends in Computer Graphics and Vision, 3(3), 177-280. https://doi.org/10.1561/0600000017


Deep Learning Approaches¶

DeTone, D., Malisiewicz, T., & Rabinovich, A. (2018). SuperPoint: Self-supervised interest point detection and description. Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition Workshops (pp. 224-236). https://doi.org/10.1109/CVPRW.2018.00060

Dusmanu, M., Rocco, I., Pajdla, T., Pollefeys, M., Sivic, J., Torii, A., & Sattler, T. (2019). D2-Net: A trainable CNN for joint description and detection of local features. Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (pp. 8092-8101). https://doi.org/10.1109/CVPR.2019.00828


Material Académico¶

Ochoa Ruiz, G. (2024). Módulo 3.2: Extracción de descriptores [Material de curso]. Tecnológico de Monterrey, Escuela de Ingeniería y Ciencias.


Recursos Web¶

Mathworks. (2024). Corner detection. MATLAB Documentation. https://www.mathworks.com/help/vision/ug/corner-detection.html

PyImageSearch. (2024). Corner detection. PyImageSearch Tutorials. https://pyimagesearch.com/


Software y Herramientas¶

Van Rossum, G., & Drake, F. L. (2009). Python 3 reference manual. CreateSpace.

Hunter, J. D. (2007). Matplotlib: A 2D graphics environment. Computing in Science & Engineering, 9(3), 90-95. https://doi.org/10.1109/MCSE.2007.55

McKinney, W. (2010). Data structures for statistical computing in Python. Proceedings of the 9th Python in Science Conference (pp. 56-61). https://doi.org/10.25080/Majora-92bf1922-00a